dashboard: system information widget

Minor restructuring of the BaseTableWidget as well
This commit is contained in:
Stephan de Wit 2024-04-04 16:47:09 +02:00
parent dbd1800584
commit d267e33de4
7 changed files with 204 additions and 40 deletions

1
plist
View File

@ -1930,6 +1930,7 @@
/usr/local/opnsense/www/js/widgets/BaseWidget.js
/usr/local/opnsense/www/js/widgets/Cpu.js
/usr/local/opnsense/www/js/widgets/Interfaces.js
/usr/local/opnsense/www/js/widgets/SystemInformation.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

@ -46,6 +46,15 @@ class DashboardController extends ApiControllerBase
],
'interfaces' => [
'title' => gettext('Interfaces'),
],
'systeminformation' => [
'title' => gettext('System Information'),
'name' => gettext('Name'),
'versions' => gettext('Versions'),
'updates' => gettext('Updates'),
'datetime' => gettext('Current date/time'),
'uptime' => gettext('Uptime'),
'config' => gettext('Last configuration change')
]
];
}

View File

@ -33,6 +33,7 @@ namespace OPNsense\Core\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\ACL;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
/**
* Class SystemController
@ -40,6 +41,24 @@ use OPNsense\Core\Backend;
*/
class SystemController extends ApiControllerBase
{
private function formatUptime($uptime)
{
$days = floor($uptime / (3600 * 24));
$hours = floor(($uptime % (3600 * 24)) / 3600);
$minutes = floor(($uptime % 3600) / 60);
$seconds = $uptime % 60;
if ($days > 0) {
$plural = $days > 1 ? gettext("days") : gettext("day");
return sprintf(
"%d %s, %02d:%02d:%02d",
$days, $plural, $hours, $minutes, $seconds
);
} else {
return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds);
}
}
public function haltAction()
{
if ($this->request->isPost()) {
@ -185,4 +204,56 @@ class SystemController extends ApiControllerBase
return ["status" => "failed"];
}
public function systemInformationAction()
{
$config = Config::getInstance()->object();
$backend = new Backend();
$product = json_decode($backend->configdRun('firmware product'), true);
$current = explode('_', $product['product_version'])[0];
/* information from changelog, more accurate for production release */
$from_changelog = strpos($product['product_id'], '-devel') === false &&
!empty($product['product_latest']) &&
$product['product_latest'] != $current;
/* update status from last check, also includes major releases */
$from_check = !empty($product['product_check']['upgrade_sets']) ||
!empty($product['product_check']['downgrade_packages']) ||
!empty($product['product_check']['new_packages']) ||
!empty($product['product_check']['reinstall_packages']) ||
!empty($product['product_check']['remove_packages']) ||
!empty($product['product_check']['upgrade_packages']);
$response = [
'name' => $config->system->hostname . '.' . $config->system->domain,
'versions' => [
sprintf('%s %s-%s', $product['product_name'], $product['product_version'], $product['product_arch']),
php_uname('s') . ' ' . php_uname('r'),
trim($backend->configdRun('system openssl version'))
],
'updates' => ($from_changelog || $from_check)
? gettext('Click to view pending updates.')
: gettext('Click to check for updates.'),
];
return json_encode($response);
}
public function systemTimeAction()
{
$config = Config::getInstance()->object();
$boottime = json_decode((new Backend())->configdRun('system sysctl values kern.boottime'), true);
preg_match("/sec = (\d+)/", $boottime['kern.boottime'], $matches);
$last_change = date("D M j G:i:s T Y", !empty($config->revision->time) ? intval($config->revision->time) : 0);
$response = [
'uptime' => $this->formatUptime(time() - $matches[1]),
'datetime' => date("D M j G:i:s T Y"),
'config' => $last_change,
];
return json_encode($response);
}
}

View File

@ -32,6 +32,8 @@
<!-- gridstack core -->
<script src="{{ cache_safe('/ui/js/gridstack-all.min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/moment-with-locales.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" />

View File

@ -114,3 +114,9 @@ command:/usr/local/opnsense/scripts/system/cpu.py
parameters:--interval %s
type:stream_output
message:Stream CPU stats
[openssl.version]
command:/usr/local/bin/openssl version | cut -f -2 -d ' '
parameters:
type:script_output
message:Show OpenSSL version

View File

@ -20,6 +20,7 @@ export default class BaseTableWidget extends BaseWidget {
'.flextable-row': {'padding': '0.5em 0.5em'},
'.header .flex-row': {'border-bottom': ''},
'.flex-row': {'width': this._calculateColumnWidth.bind(this)},
'.column .flex-row': {'width': '100%'},
'.column': {'width': ''},
'.flex-cell': {'width': ''},
}
@ -47,11 +48,26 @@ export default class BaseTableWidget extends BaseWidget {
return '';
}
_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);
}
}
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}, ...]
* left: headers are on the left of the table (key-value). Data layout: [{header1: value1}, {header2: value2}, ...].
* Supports nested columns (e.g. {header1: [value1, value2...]})
* none: no headers. Data layout: [[value1, value2], ...]
*/
this.options = {
@ -60,15 +76,12 @@ export default class BaseTableWidget extends BaseWidget {
}
}
updateTable(data = [], clear = true) {
updateTable(data = []) {
let $table = $(`#${this.flextableId}`);
if (clear) {
$table.children('.flextable-row').remove();
}
$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');
@ -80,16 +93,17 @@ export default class BaseTableWidget extends BaseWidget {
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') {
switch (this.options.headerPosition) {
case "none":
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);
break;
case "top":
let $flextableRow = $(`<div class="flextable-row"></div>`);
for (const [h, c] of Object.entries(row)) {
if (!this.headers.has(h)) {
@ -115,49 +129,35 @@ export default class BaseTableWidget extends BaseWidget {
}
}
$table.append($flextableRow);
} else if (this.options.headerPosition === 'left') {
break;
case "left":
for (const [h, c] of Object.entries(row)) {
if (Array.isArray(c)) {
// nested column
let $row = $('<div class="flextable-row" role="rowgroup"></div>');
let $row = $('<div class="flextable-row"></div>');
$row.append($(`
<div class="flex-row rowspan first" role="cell"><b>${h}</b></div>
<div class="flex-row rowspan first"><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 class="flex-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 class="flextable-row">
<div class="flex-row first"><b>${h}</b></div>
<div class="flex-row">${c}</div>
</div>
`));
`));
}
}
}
break;
}
}
}
_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);
}
}

View File

@ -0,0 +1,75 @@
// endpoint:/api/core/system/systemInformation
/**
* 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 SystemInformation extends BaseTableWidget {
constructor() {
super();
this.tickTimeout = 1000;
}
getMarkup() {
super.setTableOptions({
headerPosition: 'left'
});
return super.getMarkup();
}
async onWidgetTick() {
await ajaxGet('/api/core/system/systemTime', {}, (data, status) => {
$('#datetime').text(data['datetime']);
$('#uptime').text(data['uptime']);
$('#config').text(data['config']);
});
}
async onMarkupRendered() {
await ajaxGet('/api/core/system/systemInformation', {}, (data, status) => {
let rows = [];
for (let [key, value] of Object.entries(data)) {
if (!key in this.translations) {
console.error('Missing translation for ' + key);
continue;
}
if (key === 'updates') {
value = $('<a>').attr('href', '/ui/core/firmware#checkupdate').text(value).prop('outerHTML');
}
rows.push({[this.translations[key]]: value});
}
rows.push({[this.translations['uptime']]: $('<span id="uptime">').prop('outerHTML')});
rows.push({[this.translations['datetime']]: $('<span id="datetime">').prop('outerHTML')});
rows.push({[this.translations['config']]: $('<span id="config">').prop('outerHTML')});
super.updateTable(rows);
});
}
}