mirror of
https://github.com/lucaspalomodevelop/core.git
synced 2026-03-13 16:14:40 +00:00
dashboard: add firewall widget, change BaseTableWidget accordingly
The BaseTableWidget now contains some more rudimentary options to update existing rows and sort on a specific column index. The firewall widget counts events live as they happen and populates a table in a larger view, or a doughnut chart in a smaller view as data comes in.
This commit is contained in:
parent
0a45505611
commit
2785cb641f
1
plist
1
plist
@ -1955,6 +1955,7 @@
|
||||
/usr/local/opnsense/www/js/widgets/BaseWidget.js
|
||||
/usr/local/opnsense/www/js/widgets/Cpu.js
|
||||
/usr/local/opnsense/www/js/widgets/Disk.js
|
||||
/usr/local/opnsense/www/js/widgets/Firewall.js
|
||||
/usr/local/opnsense/www/js/widgets/InterfaceStatistics.js
|
||||
/usr/local/opnsense/www/js/widgets/Interfaces.js
|
||||
/usr/local/opnsense/www/js/widgets/Memory.js
|
||||
|
||||
@ -88,6 +88,21 @@ class DashboardController extends ApiControllerBase
|
||||
'peer' => gettext('Peer'),
|
||||
'pubkey' => gettext('Public Key'),
|
||||
'handshake' => gettext('Latest handshake'),
|
||||
],
|
||||
'firewall' => [
|
||||
'title' => gettext('Firewall'),
|
||||
'action' => gettext('Action'),
|
||||
'time' => gettext('Time'),
|
||||
'interface' => gettext('Interface'),
|
||||
'source' => gettext('Source'),
|
||||
'destination' => gettext('Destination'),
|
||||
'port' => gettext('Port'),
|
||||
'matchedrule' => gettext('Matched rule'),
|
||||
'click' => gettext('Click to track this rule in Live View'),
|
||||
'label' => gettext('Label'),
|
||||
'count' => gettext('Count'),
|
||||
'livelog' => gettext('Live Log'),
|
||||
'events' => gettext('Events'),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@ -58,6 +58,19 @@ class FirewallController extends ApiControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
public function streamLogAction()
|
||||
{
|
||||
return $this->configdStream(
|
||||
'filter stream log',
|
||||
[],
|
||||
[
|
||||
'Content-Type: text/event-stream',
|
||||
'Cache-Control: no-cache'
|
||||
],
|
||||
60
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve firewall log filter choices
|
||||
* @return array
|
||||
|
||||
@ -172,6 +172,13 @@ td {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* CPU widget */
|
||||
.cpu-type {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
/* ----- */
|
||||
|
||||
/* Custom flex table */
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
@ -241,31 +248,26 @@ div {
|
||||
.column .flex-cell:not(:last-child) {
|
||||
border-bottom: solid 1px rgba(217, 79, 0, 0.15);
|
||||
}
|
||||
|
||||
|
||||
/* ----- */
|
||||
|
||||
.cpu-type {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* CSS grid responsive table */
|
||||
.grid-table {
|
||||
margin: 2em auto;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.grid-header-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
background-color: #f7e2d6; /* fade-in color, transitions to transparent */
|
||||
border-top: 1px solid #f7e2d6;
|
||||
transition: 0.5s;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.grid-row:hover {
|
||||
background: #F5F5F5 !important;
|
||||
transition: 500ms;
|
||||
}
|
||||
|
||||
.grid-header {
|
||||
@ -278,5 +280,4 @@ div {
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
@ -31,9 +31,7 @@ export default class BaseTableWidget extends BaseWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.options = null;
|
||||
this.data = [];
|
||||
|
||||
this.tables = {};
|
||||
this.curSize = null;
|
||||
this.sizeStates = {
|
||||
0: {
|
||||
@ -53,63 +51,43 @@ export default class BaseTableWidget extends BaseWidget {
|
||||
}
|
||||
}
|
||||
this.widths = Object.keys(this.sizeStates).sort();
|
||||
|
||||
this.flextableId = Math.random().toString(36).substring(7);
|
||||
this.$flextable = null;
|
||||
this.$headerContainer = null;
|
||||
}
|
||||
|
||||
_calculateColumnWidth() {
|
||||
if (this.options !== null && this.data !== null) {
|
||||
switch (this.options.headerPosition) {
|
||||
case 'none':
|
||||
return `calc(100% / ${this.data[0].length})`;
|
||||
case 'left':
|
||||
return `calc(100% / 2)`;
|
||||
for (const [id, tableObj] of Object.entries(this.tables)) {
|
||||
if (tableObj.options.headerPosition === 'left') {
|
||||
return `calc(100% / 2)`;
|
||||
}
|
||||
|
||||
if (tableObj.options.headerPosition === 'top') {
|
||||
return `calc(100% / ${tableObj.data[0].length})`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
_constructTable() {
|
||||
if (this.options === null) {
|
||||
console.error('No table options set');
|
||||
return null;
|
||||
_rotate(id, newElement) {
|
||||
let opts = this.tables[id].options;
|
||||
let data = this.tables[id].data;
|
||||
|
||||
data.unshift(newElement);
|
||||
if (data.length > opts.rotation) {
|
||||
data.splice(opts.rotation);
|
||||
}
|
||||
|
||||
if (this.options.headerPosition === 'top') {
|
||||
this.$flextable = $(`<div class="grid-table" id="id_${this.flextableId}" role="table"></div>`)
|
||||
this.$headerContainer = $(`<div id="header_${this.flextableId}" class="grid-header-container"></div>`);
|
||||
|
||||
for (const h of this.options.headers) {
|
||||
this.$headerContainer.append($(`
|
||||
<div class="grid-item grid-header">${h}</div>
|
||||
`));
|
||||
}
|
||||
|
||||
this.$flextable.append(this.$headerContainer);
|
||||
} else {
|
||||
this.$flextable = $(`<div class="flextable-container" id="id_${this.flextableId}" role="table"></div>`)
|
||||
}
|
||||
}
|
||||
|
||||
_rotate(arr, newElement) {
|
||||
arr.unshift(newElement);
|
||||
if (arr.length > this.options.rotation) {
|
||||
arr.splice(this.options.rotation);
|
||||
}
|
||||
|
||||
const divs = document.querySelectorAll(`#id_${this.flextableId} .grid-row`);
|
||||
if (divs.length > this.options.rotation) {
|
||||
for (let i = this.options.rotation; i < divs.length; i++) {
|
||||
const divs = document.querySelectorAll(`#${id} .grid-row`);
|
||||
if (divs.length > opts.rotation) {
|
||||
for (let i = opts.rotation; i < divs.length; i++) {
|
||||
$(divs[i]).remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTableOptions(options = {}) {
|
||||
createTable(id, options) {
|
||||
/**
|
||||
* Options:
|
||||
*
|
||||
* headerPosition: top, left or none.
|
||||
* top: headers are on top of the table. Headers are defined in the options. Data layout:
|
||||
* [
|
||||
@ -126,27 +104,66 @@ export default class BaseTableWidget extends BaseWidget {
|
||||
*
|
||||
* none: no headers, same data layout as 'top', without headers set as an option.
|
||||
*
|
||||
* rotation: limit table entries to a certain amount, and rotate them. Only applicable for headerPosition: top.
|
||||
* rotation: limit table entries to a certain amount and rotate them. Only applicable for headerPosition: top.
|
||||
* headers: list of headers to display. Only applicable for headerPosition: top.
|
||||
* sortIndex: index of the column to sort on. Only applicable for headerPosition: top.
|
||||
* sortOrder: 'asc' or 'desc'. Only applicable for headerPosition: top.
|
||||
*
|
||||
*/
|
||||
if (this.options === null) {
|
||||
console.error('No table options set');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.options = {
|
||||
let mergedOpts = {
|
||||
headerPosition: 'top',
|
||||
rotation: false,
|
||||
...options // merge and override defaults
|
||||
sortIndex: null,
|
||||
sortOrder: 'desc',
|
||||
...options
|
||||
}
|
||||
|
||||
let $table = null;
|
||||
let $headerContainer = null;
|
||||
|
||||
if (mergedOpts.headerPosition === 'top') {
|
||||
/* CSS grid implementation */
|
||||
$table = $(`<div class="grid-table" id="${id}" role="table"></div>`);
|
||||
$headerContainer = $(`<div id="header_${id}" class="grid-header-container"></div>`);
|
||||
|
||||
for (const h of mergedOpts.headers) {
|
||||
$headerContainer.append($(`
|
||||
<div class="grid-item grid-header">${h}</div>
|
||||
`));
|
||||
}
|
||||
|
||||
$table.append($headerContainer);
|
||||
} else {
|
||||
/* flextable implementation */
|
||||
$table = $(`<div class="flextable-container" id="${id}" role="table"></div>`);
|
||||
}
|
||||
|
||||
this.tables[id] = {
|
||||
'table': $table,
|
||||
'options': mergedOpts,
|
||||
'headerContainer': $headerContainer,
|
||||
'data': [],
|
||||
};
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
updateTable(data = []) {
|
||||
let $table = $(`#id_${this.flextableId}`);
|
||||
updateTable(id, data = [], rowIdentifier = null) {
|
||||
let $table = $(`#${id}`);
|
||||
let options = this.tables[id].options;
|
||||
|
||||
if (!this.options.rotation) {
|
||||
if (!options.rotation) {
|
||||
$table.children('.flextable-row').remove();
|
||||
this.data = data;
|
||||
this.tables[id].data = data;
|
||||
}
|
||||
|
||||
for (const row of data) {
|
||||
switch (this.options.headerPosition) {
|
||||
switch (options.headerPosition) {
|
||||
case "none":
|
||||
let $row = $(`<div class="flextable-row"></div>`)
|
||||
for (const item of row) {
|
||||
@ -157,15 +174,45 @@ export default class BaseTableWidget extends BaseWidget {
|
||||
$table.append($row);
|
||||
break;
|
||||
case "top":
|
||||
let $gridRow = $(`<div class="grid-row" style="opacity: 0.4; background-color: #f7e2d6"></div>`);
|
||||
for (const item of row) {
|
||||
$gridRow.append($(`
|
||||
<div class="grid-item">${item}</div>
|
||||
`));
|
||||
let $gridRow = $(`<div class="grid-row"></div>`);
|
||||
let newElement = true;
|
||||
if (rowIdentifier !== null) {
|
||||
$gridRow = $(`#id_${rowIdentifier}`);
|
||||
if ($gridRow.length === 0) {
|
||||
$gridRow = $(`<div class="grid-row" id="id_${rowIdentifier}"></div>`);
|
||||
} else {
|
||||
newElement = false;
|
||||
$gridRow.empty();
|
||||
}
|
||||
}
|
||||
|
||||
$(`#header_${this.flextableId}`).after($gridRow);
|
||||
if (this.options.rotation) {
|
||||
let i = 0;
|
||||
for (const item of row) {
|
||||
$gridRow.append($(`
|
||||
<div class="grid-item ${(options.sortIndex !== null && options.sortIndex == i) ? 'sort' : ''}">${item}</div>
|
||||
`));
|
||||
i++;
|
||||
}
|
||||
|
||||
if (newElement) {
|
||||
$(`#header_${id}`).after($gridRow);
|
||||
}
|
||||
|
||||
if (options.sortIndex !== null) {
|
||||
let items = $table.children('.grid-row').toArray().sort(function(a, b) {
|
||||
let vA = parseInt($(a).children('.sort').first().text());
|
||||
let vB = parseInt($(b).children('.sort').first().text());
|
||||
|
||||
if (options.sortOrder === 'asc')
|
||||
return (vA < vB) ? -1 : (vA > vB) ? 1 : 0;
|
||||
else {
|
||||
return (vA > vB) ? -1 : (vA < vB) ? 1 : 0;
|
||||
}
|
||||
});
|
||||
$table.append(items);
|
||||
}
|
||||
|
||||
if (options.rotation) {
|
||||
$gridRow.animate({
|
||||
from: 0,
|
||||
to: 255,
|
||||
@ -174,11 +221,12 @@ export default class BaseTableWidget extends BaseWidget {
|
||||
duration: 50,
|
||||
easing: 'linear',
|
||||
step: function(now) {
|
||||
$gridRow.css('background-color',`transparent`)
|
||||
$gridRow.css('background-color','initial')
|
||||
}
|
||||
});
|
||||
this._rotate(this.data, row);
|
||||
this._rotate(id, row);
|
||||
}
|
||||
|
||||
break;
|
||||
case "left":
|
||||
if (row.length !== 2) {
|
||||
@ -213,15 +261,6 @@ export default class BaseTableWidget extends BaseWidget {
|
||||
}
|
||||
}
|
||||
|
||||
getMarkup() {
|
||||
this._constructTable();
|
||||
return $(this.$flextable);
|
||||
}
|
||||
|
||||
async onMarkupRendered() {
|
||||
|
||||
}
|
||||
|
||||
onWidgetResize(elem, width, height) {
|
||||
let lowIndex = 0;
|
||||
for (let i = 0; i < this.widths.length; i++) {
|
||||
|
||||
278
src/opnsense/www/js/widgets/Firewall.js
Normal file
278
src/opnsense/www/js/widgets/Firewall.js
Normal file
@ -0,0 +1,278 @@
|
||||
// endpoint:/api/diagnostics/firewall/streamLog
|
||||
// endpoint:/api/diagnostics/interface/getInterfaceNames
|
||||
|
||||
/**
|
||||
* 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 Firewall extends BaseTableWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.eventSource = null;
|
||||
this.ifMap = {};
|
||||
this.counters = {};
|
||||
this.chart = null;
|
||||
}
|
||||
|
||||
getMarkup() {
|
||||
let $container = $('<div></div>');
|
||||
let $tableContainer = $(`<div id="fw-table-container"><b>${this.translations.livelog}</b></div>`);
|
||||
let $top_table = this.createTable('fw-top-table', {
|
||||
headerPosition: 'top',
|
||||
rotation: 5,
|
||||
headers: [
|
||||
this.translations.action,
|
||||
this.translations.time,
|
||||
this.translations.interface,
|
||||
this.translations.source,
|
||||
this.translations.destination,
|
||||
this.translations.port
|
||||
],
|
||||
});
|
||||
|
||||
let $rule_table = this.createTable('fw-rule-table', {
|
||||
headerPosition: 'top',
|
||||
rotation: 5,
|
||||
headers: [
|
||||
this.translations.label,
|
||||
this.translations.count
|
||||
],
|
||||
sortIndex: 1,
|
||||
sortOrder: 'desc'
|
||||
});
|
||||
|
||||
$tableContainer.append($top_table);
|
||||
$tableContainer.append(`<div style="margin-top: 2em"><b>${this.translations.events}</b><div>`);
|
||||
$tableContainer.append($rule_table);
|
||||
|
||||
$container.append($tableContainer);
|
||||
|
||||
$container.append($(`
|
||||
<div class="fw-chart-container">
|
||||
<div class="canvas-container">
|
||||
<canvas id="fw-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
`));
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
_onMessage(event) {
|
||||
if (!event) {
|
||||
this.eventSource.close();
|
||||
}
|
||||
|
||||
let actIcons = {
|
||||
'pass': '<i class="fa fa-play text-success"></i>',
|
||||
'block': '<i class="fa fa-minus-circle text-danger"></i>',
|
||||
'rdr': '<i class="fa fa-exchange text-info"></i>',
|
||||
'nat': '<i class="fa fa-exchange text-info"></i>',
|
||||
}
|
||||
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// increase counters
|
||||
if (!this.counters[data.rid]) {
|
||||
this.counters[data.rid] = {
|
||||
count: 1,
|
||||
label: data.label ?? ''
|
||||
}
|
||||
} else {
|
||||
this.counters[data.rid].count++;
|
||||
}
|
||||
|
||||
let popContent = $(`
|
||||
<p>
|
||||
@${data.rulenr}
|
||||
${data.label.length > 0 ? 'Label: ' + data.label : ''}
|
||||
<br>
|
||||
<sub>${this.translations.click}</sub>
|
||||
</p>
|
||||
`).prop('outerHTML');
|
||||
let popover = $(`
|
||||
<a target="_blank" href="/ui/diagnostics/firewall/log?rid=${data.rid}" type="button"
|
||||
data-toggle="popover" data-trigger="hover" data-html="true" data-title="${this.translations.matchedrule}"
|
||||
data-content="${popContent}">
|
||||
${actIcons[data.action]}
|
||||
</a>
|
||||
`);
|
||||
|
||||
super.updateTable('fw-top-table', [
|
||||
[
|
||||
popover.prop('outerHTML'),
|
||||
/* Format time based on client browser locale */
|
||||
(new Intl.DateTimeFormat(undefined, {hour: 'numeric', minute: 'numeric'})).format(new Date(data.__timestamp__)),
|
||||
this.ifMap[data.interface] ?? data.interface,
|
||||
data.src,
|
||||
data.dst,
|
||||
data.dstport ?? ''
|
||||
]
|
||||
]);
|
||||
|
||||
$('[data-toggle="popover"]').popover('hide');
|
||||
$('[data-toggle="popover"]').popover({
|
||||
container: 'body'
|
||||
}).on('show.bs.popover', function() {
|
||||
$(this).data("bs.popover").tip().css("max-width", "100%")
|
||||
});
|
||||
|
||||
super.updateTable('fw-rule-table', [
|
||||
[
|
||||
$('<div style="text-align: left;"></div>').text(this.counters[data.rid].label).prop('outerHTML'),
|
||||
this.counters[data.rid].count
|
||||
]
|
||||
], data.rid);
|
||||
|
||||
this._updateChart(data.rid, this.counters[data.rid].label, this.counters[data.rid].count);
|
||||
}
|
||||
|
||||
_updateChart(rid, label, count) {
|
||||
let labels = this.chart.data.labels;
|
||||
let data = this.chart.data.datasets[0].data;
|
||||
let rids = this.chart.data.datasets[0].rids;
|
||||
|
||||
let idx = rids.findIndex(x => x === rid);
|
||||
if (idx === -1) {
|
||||
labels.push(label);
|
||||
data.push(count);
|
||||
rids.push(rid);
|
||||
} else {
|
||||
data[idx] = count;
|
||||
}
|
||||
|
||||
this.chart.update();
|
||||
}
|
||||
|
||||
async onMarkupRendered() {
|
||||
let exit = false;
|
||||
ajaxGet('/api/diagnostics/interface/getInterfaceNames', {}, (data, status) => {
|
||||
if (status !== 'success') {
|
||||
console.error('Failed to fetch interface descriptions');
|
||||
exit = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.ifMap = data;
|
||||
});
|
||||
|
||||
if (exit) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventSource = new EventSource('/api/diagnostics/firewall/streamLog');
|
||||
this.eventSource.onmessage = this._onMessage.bind(this);
|
||||
|
||||
let context = document.getElementById('fw-chart').getContext('2d');
|
||||
let config = {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
data: [],
|
||||
rids: [],
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
cutout: '40%',
|
||||
maintainAspectRatio: true,
|
||||
responsive: true,
|
||||
aspectRatio: 2,
|
||||
layout: {
|
||||
padding: 10
|
||||
},
|
||||
normalized: true,
|
||||
parsing: false,
|
||||
onClick: (event, elements, chart) => {
|
||||
const i = elements[0].index;
|
||||
const rid = chart.data.datasets[0].rids[i];
|
||||
window.open(`/ui/diagnostics/firewall/log?rid=${rid}`);
|
||||
},
|
||||
onHover: (event, elements) => {
|
||||
event.native.target.style.cursor = elements[0] ? 'pointer' : 'grab';
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'left',
|
||||
onHover: (event, legendItem) => {
|
||||
const activeElement = {
|
||||
datasetIndex: 0,
|
||||
index: legendItem.index
|
||||
};
|
||||
this.chart.setActiveElements([activeElement]);
|
||||
this.chart.tooltip.setActiveElements([activeElement]);
|
||||
},
|
||||
labels: {
|
||||
filter: (ds, data) => {
|
||||
/* clamp amount of legend labels to a max of 10 (sorted) */
|
||||
const sortable = [];
|
||||
data.labels.forEach((l, i) => {
|
||||
sortable.push([l, data.datasets[0].data[i]]);
|
||||
});
|
||||
sortable.sort((a, b) => (b[1] - a[1]));
|
||||
const sorted = sortable.slice(0, 10).map(e => (e[0]));
|
||||
|
||||
return sorted.includes(ds.text)
|
||||
},
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
labels: (tooltipItem) => {
|
||||
let obj = this.counters[tooltipItem.label];
|
||||
return `${obj.label} (${obj.count})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.chart = new Chart(context, config);
|
||||
}
|
||||
|
||||
onWidgetClose() {
|
||||
if (this.eventSource !== null) {
|
||||
this.eventSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
onWidgetResize(elem, width, height) {
|
||||
if (width < 650) {
|
||||
$('#fw-chart').show();
|
||||
$('#fw-table-container').hide();
|
||||
} else {
|
||||
$('#fw-chart').hide();
|
||||
$('#fw-table-container').show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,12 +43,13 @@ export default class Interfaces extends BaseTableWidget {
|
||||
}
|
||||
|
||||
getMarkup() {
|
||||
let options = {
|
||||
let $container = $('<div></div>');
|
||||
let $if_table = this.createTable('if-table', {
|
||||
headerPosition: 'none'
|
||||
}
|
||||
});
|
||||
|
||||
super.setTableOptions(options);
|
||||
return super.getMarkup();
|
||||
$container.append($if_table);
|
||||
return $container;
|
||||
}
|
||||
|
||||
async onMarkupRendered() {
|
||||
@ -102,7 +103,7 @@ export default class Interfaces extends BaseTableWidget {
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
super.updateTable(rows);
|
||||
super.updateTable('if-table', rows);
|
||||
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
|
||||
@ -36,10 +36,12 @@ export default class SystemInformation extends BaseTableWidget {
|
||||
}
|
||||
|
||||
getMarkup() {
|
||||
super.setTableOptions({
|
||||
headerPosition: 'left'
|
||||
let $container = $('<div></div>');
|
||||
let $sysinfotable = this.createTable('sysinfo-table', {
|
||||
headerPosition: 'left',
|
||||
});
|
||||
return super.getMarkup();
|
||||
$container.append($sysinfotable);
|
||||
return $container;
|
||||
}
|
||||
|
||||
async onWidgetTick() {
|
||||
@ -69,7 +71,7 @@ export default class SystemInformation extends BaseTableWidget {
|
||||
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);
|
||||
super.updateTable('sysinfo-table', rows);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user