mirror of
https://github.com/lucaspalomodevelop/opnsense-core.git
synced 2026-03-13 08:09:42 +00:00
dashboard: add widget configuration options
This commit is contained in:
parent
7c6e958897
commit
104fcf9412
@ -64,11 +64,13 @@ $( document ).ready(function() {
|
||||
}
|
||||
}, {
|
||||
'save': "{{ lang._('Save') }}",
|
||||
'ok': "{{ lang._('OK') }}",
|
||||
'restore': "{{ lang._('Restore default layout') }}",
|
||||
'addwidget': "{{ lang._('Add Widget') }}",
|
||||
'add': "{{ lang._('Add') }}",
|
||||
'cancel': "{{ lang._('Cancel') }}",
|
||||
'failed': "{{ lang._('Failed to load widget') }}",
|
||||
'options': "{{ lang._('Options') }}",
|
||||
});
|
||||
widgetManager.initialize();
|
||||
});
|
||||
|
||||
@ -257,61 +257,8 @@ class WidgetManager {
|
||||
$('#save-grid').hide();
|
||||
|
||||
// Click event for save button
|
||||
$('#save-grid').click(() => {
|
||||
// Show the spinner when the save operation starts
|
||||
$('#save-btn-text').toggleClass("show hide");
|
||||
$('#save-spinner').addClass('show');
|
||||
$('#save-grid').prop('disabled', true);
|
||||
|
||||
let items = this.grid.save(false);
|
||||
items.forEach((item) => {
|
||||
// Store widget-specific configuration
|
||||
let widgetConfig = this.widgetClasses[item.id].getWidgetConfig();
|
||||
if (widgetConfig) {
|
||||
item['widget'] = widgetConfig;
|
||||
}
|
||||
|
||||
// XXX the gridstack save() behavior is inconsistent with the responsive columnWidth option,
|
||||
// as the calculation will return impossible values for the x, y, w and h attributes.
|
||||
// For now, the gs-{x,y,w,h} attributes are a better representation of the grid for layout persistence
|
||||
let elem = $(this.widgetHTMLElements[item.id]);
|
||||
item.x = parseInt(elem.attr('gs-x')) ?? 1;
|
||||
item.y = parseInt(elem.attr('gs-y')) ?? 1;
|
||||
item.w = parseInt(elem.attr('gs-w')) ?? 1;
|
||||
item.h = parseInt(elem.attr('gs-h')) ?? 1;
|
||||
|
||||
delete item['callbacks'];
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/core/dashboard/saveWidgets",
|
||||
dataType: "text",
|
||||
contentType: 'text/plain',
|
||||
data: JSON.stringify(items),
|
||||
complete: (data, status) => {
|
||||
setTimeout(() => {
|
||||
let response = JSON.parse(data.responseText);
|
||||
|
||||
if (response['result'] == 'failed') {
|
||||
console.error('Failed to save widgets', data);
|
||||
$('#save-grid').prop('disabled', false);
|
||||
$('#save-spinner').removeClass('show').addClass('hide');
|
||||
$('#save-btn-text').removeClass('hide').addClass('show');
|
||||
} else {
|
||||
$('#save-spinner').removeClass('show').addClass('hide');
|
||||
$('#save-check').toggleClass("hide show");
|
||||
setTimeout(() => {
|
||||
// Hide the save button upon successful save
|
||||
$('#save-grid').hide();
|
||||
$('#save-check').toggleClass("show hide");
|
||||
$('#save-btn-text').toggleClass("hide show");
|
||||
$('#save-grid').prop('disabled', false);
|
||||
}, 500)
|
||||
}
|
||||
}, 300); // Artificial delay to give more feedback on button click
|
||||
}
|
||||
});
|
||||
$('#save-grid').click(async () => {
|
||||
await this._saveDashboard();
|
||||
});
|
||||
|
||||
$('#add_widget').click(() => {
|
||||
@ -339,11 +286,12 @@ class WidgetManager {
|
||||
BootstrapDialog.show({
|
||||
title: this.gettext.addwidget,
|
||||
draggable: true,
|
||||
animate: false,
|
||||
message: $content,
|
||||
buttons: [{
|
||||
label: this.gettext.add,
|
||||
hotkey: 13,
|
||||
action: async (dialog) => {
|
||||
action: (dialog) => {
|
||||
let ids = $('select', dialog.$modalContent).val();
|
||||
let changed = false;
|
||||
for (const id of ids) {
|
||||
@ -445,8 +393,7 @@ class WidgetManager {
|
||||
$(`.spinner-${widget.id}`).remove();
|
||||
|
||||
// retrieve widget-specific options
|
||||
const options = widget.getWidgetOptions();
|
||||
if (!$.isEmptyObject(options)) {
|
||||
if (widget.isConfigurable()) {
|
||||
let $editHandle = $(`
|
||||
<div id="edit-handle-${widget.id}" class="edit-handle">
|
||||
<i class="fa fa-pencil"></i>
|
||||
@ -454,8 +401,8 @@ class WidgetManager {
|
||||
`);
|
||||
$(`#close-handle-${widget.id}`).before($editHandle);
|
||||
|
||||
$editHandle.click((event) => {
|
||||
// TODO: implement
|
||||
$editHandle.on('click', async (event) => {
|
||||
await this._renderOptionsForm(widget);
|
||||
});
|
||||
}
|
||||
|
||||
@ -553,6 +500,142 @@ class WidgetManager {
|
||||
return $panel;
|
||||
}
|
||||
|
||||
async _saveDashboard() {
|
||||
// Show the spinner when the save operation starts
|
||||
$('#save-btn-text').toggleClass("show hide");
|
||||
$('#save-spinner').addClass('show');
|
||||
$('#save-grid').prop('disabled', true);
|
||||
|
||||
let items = this.grid.save(false);
|
||||
items = await Promise.all(items.map(async (item) => {
|
||||
let widgetConfig = await this.widgetClasses[item.id].getWidgetConfig();
|
||||
if (widgetConfig) {
|
||||
item['widget'] = widgetConfig;
|
||||
}
|
||||
|
||||
// XXX the gridstack save() behavior is inconsistent with the responsive columnWidth option,
|
||||
// as the calculation will return impossible values for the x, y, w and h attributes.
|
||||
// For now, the gs-{x,y,w,h} attributes are a better representation of the grid for layout persistence
|
||||
let elem = $(this.widgetHTMLElements[item.id]);
|
||||
item.x = parseInt(elem.attr('gs-x')) ?? 1;
|
||||
item.y = parseInt(elem.attr('gs-y')) ?? 1;
|
||||
item.w = parseInt(elem.attr('gs-w')) ?? 1;
|
||||
item.h = parseInt(elem.attr('gs-h')) ?? 1;
|
||||
|
||||
delete item['callbacks'];
|
||||
return item;
|
||||
}));
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/core/dashboard/saveWidgets",
|
||||
dataType: "text",
|
||||
contentType: 'text/plain',
|
||||
data: JSON.stringify(items),
|
||||
complete: (data, status) => {
|
||||
setTimeout(() => {
|
||||
let response = JSON.parse(data.responseText);
|
||||
|
||||
if (response['result'] == 'failed') {
|
||||
console.error('Failed to save widgets', data);
|
||||
$('#save-grid').prop('disabled', false);
|
||||
$('#save-spinner').removeClass('show').addClass('hide');
|
||||
$('#save-btn-text').removeClass('hide').addClass('show');
|
||||
} else {
|
||||
$('#save-spinner').removeClass('show').addClass('hide');
|
||||
$('#save-check').toggleClass("hide show");
|
||||
setTimeout(() => {
|
||||
// Hide the save button upon successful save
|
||||
$('#save-grid').hide();
|
||||
$('#save-check').toggleClass("show hide");
|
||||
$('#save-btn-text').toggleClass("hide show");
|
||||
$('#save-grid').prop('disabled', false);
|
||||
}, 500)
|
||||
}
|
||||
}, 300); // Artificial delay to give more feedback on button click
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _renderOptionsForm(widget) {
|
||||
let $content = $(`<div class="widget-options"></div>`);
|
||||
|
||||
// parse widget options
|
||||
const options = await widget.getWidgetOptions();
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
let $option = $(`<div class="widget-option-container"></div>`);
|
||||
switch (value.type) {
|
||||
case 'select_multiple':
|
||||
let $select = $(`<select class="widget_optionsform_selectpicker"
|
||||
id="${value.id}"
|
||||
data-container="body"
|
||||
class="selectpicker"
|
||||
multiple="multiple"></select>`);
|
||||
|
||||
for (const option of value.options) {
|
||||
$select.append($(`<option value="${option.value}" ${option.selected ? 'selected' : ''}>${option.value}</option>`));
|
||||
}
|
||||
|
||||
if (value.options.every(obj => !obj.selected)) {
|
||||
// No selection, apply the default.
|
||||
$select.val(value.default);
|
||||
}
|
||||
|
||||
$option.append($(`<div><b>${value.title}</b></div>`));
|
||||
$option.append($select);
|
||||
break;
|
||||
default:
|
||||
console.error('Unknown option type', value.type);
|
||||
continue;
|
||||
}
|
||||
|
||||
$content.append($option);
|
||||
}
|
||||
|
||||
// present widget options
|
||||
BootstrapDialog.show({
|
||||
title: this.gettext.options,
|
||||
draggable: true,
|
||||
animate: false,
|
||||
message: $content,
|
||||
buttons: [{
|
||||
label: this.gettext.ok,
|
||||
hotkey: 13,
|
||||
action: (dialog) => {
|
||||
let values = {};
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
switch (value.type) {
|
||||
case 'select_multiple':
|
||||
values[key] = $(`#${value.id}`).val();
|
||||
if (values[key].count === 0) {
|
||||
values[key] = value.default;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error('Unknown option type', value.type);
|
||||
}
|
||||
}
|
||||
|
||||
widget.setWidgetConfig(values);
|
||||
widget.onWidgetOptionsChanged(values);
|
||||
$('#save-grid').show();
|
||||
dialog.close();
|
||||
}
|
||||
}, {
|
||||
label: this.gettext.cancel,
|
||||
action: (dialog) => {
|
||||
dialog.close();
|
||||
}
|
||||
}],
|
||||
onshown: function(dialog) {
|
||||
$('.widget_optionsform_selectpicker').selectpicker();
|
||||
},
|
||||
onhide: function(dialog) {
|
||||
$('.widget_optionsform_selectpicker').selectpicker('destroy');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_onWidgetClose(id) {
|
||||
clearInterval(this.widgetTickRoutines[id]);
|
||||
this.widgetClasses[id].onWidgetClose();
|
||||
|
||||
@ -40,6 +40,8 @@ export default class BaseWidget {
|
||||
this.timeoutPeriod = 1000;
|
||||
this.retryLimit = 3;
|
||||
this.eventSourceRetryCount = 0; // retrycount for $.ajax is managed in its own scope
|
||||
|
||||
this.configurable = false;
|
||||
}
|
||||
|
||||
/* Public functions */
|
||||
@ -48,12 +50,30 @@ export default class BaseWidget {
|
||||
return this.resizeHandles;
|
||||
}
|
||||
|
||||
getWidgetConfig() {
|
||||
if (this.config !== undefined && 'widget' in this.config) {
|
||||
return this.config['widget'];
|
||||
}
|
||||
setWidgetConfig(config) {
|
||||
this.config['widget'] = config;
|
||||
}
|
||||
|
||||
return false;
|
||||
async getWidgetConfig() {
|
||||
let widget_config = {};
|
||||
if (this.config !== undefined && 'widget' in this.config) {
|
||||
widget_config = this.config['widget'];
|
||||
}
|
||||
const options = await this.getWidgetOptions();
|
||||
return Object.entries(options).reduce((acc, [key, value]) => {
|
||||
if (key in widget_config &&
|
||||
widget_config[key] !== null &&
|
||||
widget_config[key] !== undefined &&
|
||||
(typeof(widget_config[key] === 'array') && widget_config[key].length !== 0) &&
|
||||
(typeof(widget_config[key] === 'object') && Object.keys(widget_config[key]).length !== 0) &&
|
||||
(typeof(widget_config[key] === 'string') && widget_config[key].length !== 0)
|
||||
) {
|
||||
acc[key] = widget_config[key];
|
||||
} else {
|
||||
acc[key] = value.default;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
setId(id) {
|
||||
@ -64,17 +84,17 @@ export default class BaseWidget {
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
/* Public virtual functions */
|
||||
isConfigurable() {
|
||||
return this.configurable;
|
||||
}
|
||||
|
||||
/* Public virtual/override functions */
|
||||
|
||||
getGridOptions() {
|
||||
// per-widget gridstack options override
|
||||
return {};
|
||||
}
|
||||
|
||||
getWidgetOptions() {
|
||||
return {};
|
||||
}
|
||||
|
||||
getMarkup() {
|
||||
return $("");
|
||||
}
|
||||
@ -105,6 +125,14 @@ export default class BaseWidget {
|
||||
}
|
||||
}
|
||||
|
||||
async getWidgetOptions() {
|
||||
return {};
|
||||
}
|
||||
|
||||
onWidgetOptionsChanged(options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Utility/protected functions */
|
||||
|
||||
ajaxCall(url, data={}, method='GET') {
|
||||
@ -155,10 +183,6 @@ export default class BaseWidget {
|
||||
return false;
|
||||
}
|
||||
|
||||
setWidgetConfig(config) {
|
||||
this.config['widget'] = config;
|
||||
}
|
||||
|
||||
openEventSource(url, onMessage) {
|
||||
this.closeEventSource();
|
||||
|
||||
|
||||
@ -71,6 +71,7 @@
|
||||
<title>Traffic Graph</title>
|
||||
<trafficin>Traffic In</trafficin>
|
||||
<trafficout>Traffic Out</trafficout>
|
||||
<interfaces>Interfaces</interfaces>
|
||||
</translations>
|
||||
</traffic>
|
||||
<memory>
|
||||
|
||||
@ -27,8 +27,8 @@
|
||||
import BaseWidget from "./BaseWidget.js";
|
||||
|
||||
export default class Traffic extends BaseWidget {
|
||||
constructor() {
|
||||
super();
|
||||
constructor(config) {
|
||||
super(config);
|
||||
|
||||
this.charts = {
|
||||
trafficIn: null,
|
||||
@ -36,6 +36,8 @@ export default class Traffic extends BaseWidget {
|
||||
};
|
||||
this.initialized = false;
|
||||
this.datasets = {inbytes: [], outbytes: []};
|
||||
this.configurable = true;
|
||||
this.configChanged = false;
|
||||
}
|
||||
|
||||
_set_alpha(color, opacity) {
|
||||
@ -54,7 +56,7 @@ export default class Traffic extends BaseWidget {
|
||||
maintainAspectRatio: false,
|
||||
scaleShowLabels: false,
|
||||
tooltipEvents: [],
|
||||
pointDot: false,
|
||||
pointDot: true,
|
||||
scaleShowGridLines: true,
|
||||
responsive: true,
|
||||
normalized: true,
|
||||
@ -121,7 +123,9 @@ export default class Traffic extends BaseWidget {
|
||||
};
|
||||
}
|
||||
|
||||
_initialize(data) {
|
||||
async _initialize(data) {
|
||||
const config = await this.getWidgetConfig();
|
||||
this.datasets = {inbytes: [], outbytes: []};
|
||||
for (const dir of ['inbytes', 'outbytes']) {
|
||||
let colors = Chart.colorschemes.tableau.Classic10;
|
||||
let i = 0;
|
||||
@ -130,7 +134,7 @@ export default class Traffic extends BaseWidget {
|
||||
i++;
|
||||
this.datasets[dir].push({
|
||||
label: data.interfaces[intf].name,
|
||||
hidden: false, // XXX
|
||||
hidden: !config.interfaces.includes(data.interfaces[intf].name),
|
||||
borderColor: colors[idx],
|
||||
backgroundColor: this._set_alpha(colors[idx], 0.5),
|
||||
pointHoverBackgroundColor: colors[idx],
|
||||
@ -147,10 +151,9 @@ export default class Traffic extends BaseWidget {
|
||||
|
||||
this.charts.trafficIn = new Chart($('#traffic-in')[0].getContext('2d'), this._chartConfig(this.datasets.inbytes));
|
||||
this.charts.trafficOut = new Chart($('#traffic-out')[0].getContext('2d'), this._chartConfig(this.datasets.outbytes));
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
_onMessage(event) {
|
||||
async _onMessage(event) {
|
||||
if (!event) {
|
||||
super.closeEventSource();
|
||||
}
|
||||
@ -158,15 +161,20 @@ export default class Traffic extends BaseWidget {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (!this.initialized) {
|
||||
this._initialize(data);
|
||||
await this._initialize(data);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
for (let chart of Object.values(this.charts)) {
|
||||
Object.keys(data.interfaces).forEach((intf) => {
|
||||
chart.config.data.datasets.forEach((dataset) => {
|
||||
chart.config.data.datasets.forEach(async (dataset) => {
|
||||
if (dataset.intf === intf) {
|
||||
let elapsed_time = data.time - dataset.last_time;
|
||||
if (this.configChanged) {
|
||||
// check hidden status of dataset
|
||||
const config = await this.getWidgetConfig();
|
||||
dataset.hidden = !config.interfaces.includes(data.interfaces[intf].name);
|
||||
}
|
||||
dataset.data.push({
|
||||
x: new Date(data.time * 1000.0),
|
||||
y: Math.round(((data.interfaces[intf][dataset.src_field]) / elapsed_time) * 8, 0)
|
||||
@ -178,6 +186,10 @@ export default class Traffic extends BaseWidget {
|
||||
});
|
||||
chart.update('quiet');
|
||||
}
|
||||
|
||||
if (this.configChanged) {
|
||||
this.configChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
getMarkup() {
|
||||
@ -199,6 +211,29 @@ export default class Traffic extends BaseWidget {
|
||||
super.openEventSource('/api/diagnostics/traffic/stream/1', this._onMessage.bind(this));
|
||||
}
|
||||
|
||||
async getWidgetOptions() {
|
||||
const interfaces = await this.ajaxCall('/api/diagnostics/traffic/interface');
|
||||
return {
|
||||
interfaces: {
|
||||
title: this.translations.interfaces,
|
||||
type: 'select_multiple',
|
||||
options: Object.entries(interfaces.interfaces).map(([key,intf]) => {
|
||||
return {
|
||||
value: intf.name,
|
||||
selected: this.config.widget?.interfaces?.includes(intf.name) ?? false
|
||||
};
|
||||
}),
|
||||
default: Object.entries(interfaces.interfaces)
|
||||
.filter(([key, intf]) => key === 'lan' || key === 'wan')
|
||||
.map(([key, intf]) => (intf.name))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onWidgetOptionsChanged(options) {
|
||||
this.configChanged = true;
|
||||
}
|
||||
|
||||
onWidgetClose() {
|
||||
super.onWidgetClose();
|
||||
this.charts.trafficIn.destroy();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user