diff --git a/appserver/monitoring-console/webapp/pom.xml b/appserver/monitoring-console/webapp/pom.xml index 6e8498dc013..ebaa0591d16 100644 --- a/appserver/monitoring-console/webapp/pom.xml +++ b/appserver/monitoring-console/webapp/pom.xml @@ -113,9 +113,10 @@ true src/main/webapp/monitoring-console.js - src/main/webapp/mc-ui.js - src/main/webapp/mc-render.js - src/main/webapp/mc-page.js + src/main/webapp/mc-main.js + src/main/webapp/mc-model.js + src/main/webapp/mc-line-chart.js + src/main/webapp/mc-view.js diff --git a/appserver/monitoring-console/webapp/src/main/webapp/index.html b/appserver/monitoring-console/webapp/src/main/webapp/index.html index 71d09d6124a..b98a8ccfde6 100644 --- a/appserver/monitoring-console/webapp/src/main/webapp/index.html +++ b/appserver/monitoring-console/webapp/src/main/webapp/index.html @@ -47,85 +47,44 @@ - - + + - +
- +     - - - + + +     - - - - + + + +     - - - - + + + +
-
- - - - - - - -
Page
Name
Widgets
- - - - - -
Data
Instances
- - - - - - - - - - -
Layout
Span
Column
- - - - - - - - - - - - - - -
Render Options
Begin at Zero
Automatic Labels
Use Bezier Curves
Use Animations
Label X-Axis at 90°
Per Second
Chart Options
Show Average
Show Minimum
Show Maximum
Show Legend
-
+
diff --git a/appserver/monitoring-console/webapp/src/main/webapp/mc-render.js b/appserver/monitoring-console/webapp/src/main/webapp/mc-line-chart.js similarity index 88% rename from appserver/monitoring-console/webapp/src/main/webapp/mc-render.js rename to appserver/monitoring-console/webapp/src/main/webapp/mc-line-chart.js index 87147b9aed7..10f6572acfc 100644 --- a/appserver/monitoring-console/webapp/src/main/webapp/mc-render.js +++ b/appserver/monitoring-console/webapp/src/main/webapp/mc-line-chart.js @@ -40,7 +40,7 @@ /*jshint esversion: 8 */ -var MonitoringConsoleRender = (function() { +MonitoringConsole.LineChart = (function() { const DEFAULT_BG_COLORS = [ 'rgba(153, 102, 255, 0.2)', @@ -60,8 +60,8 @@ var MonitoringConsoleRender = (function() { ]; function createMinimumLineDataset(data, points, lineColor) { - var min = data.observedMin; - var minPoints = [{t:points[0].t, y:min}, {t:points[points.length-1].t, y:min}]; + let min = data.observedMin; + let minPoints = [{t:points[0].t, y:min}, {t:points[points.length-1].t, y:min}]; return { data: minPoints, @@ -75,8 +75,8 @@ var MonitoringConsoleRender = (function() { } function createMaximumLineDataset(data, points, lineColor) { - var max = data.observedMax; - var maxPoints = [{t:points[0].t, y:max}, {t:points[points.length-1].t, y:max}]; + let max = data.observedMax; + let maxPoints = [{t:points[0].t, y:max}, {t:points[points.length-1].t, y:max}]; return { data: maxPoints, @@ -89,8 +89,8 @@ var MonitoringConsoleRender = (function() { } function createAverageLineDataset(data, points, lineColor) { - var avg = data.observedSum / data.observedValues; - var avgPoints = [{t:points[0].t, y:avg}, {t:points[points.length-1].t, y:avg}]; + let avg = data.observedSum / data.observedValues; + let avgPoints = [{t:points[0].t, y:avg}, {t:points[points.length-1].t, y:avg}]; return { data: avgPoints, @@ -117,7 +117,7 @@ var MonitoringConsoleRender = (function() { if (!points1d) return []; let points2d = new Array(points1d.length / 2); - for (var i = 0; i < points2d.length; i++) { + for (let i = 0; i < points2d.length; i++) { points2d[i] = { t: new Date(points1d[i*2]), y: points1d[i*2+1] }; } return points2d; @@ -127,7 +127,7 @@ var MonitoringConsoleRender = (function() { if (!points1d) return []; let points2d = new Array((points1d.length / 2) - 1); - for (var i = 0; i < points2d.length; i++) { + for (let i = 0; i < points2d.length; i++) { let t0 = points1d[i*2]; let t1 = points1d[i*2+2]; let y0 = points1d[i*2+1]; @@ -144,7 +144,7 @@ var MonitoringConsoleRender = (function() { if (widget.options.perSec) { return [ createMainLineDataset(data, createInstancePerSecPoints(data.points), lineColor, bgColor) ]; } - let points = createInstancePoints(data.points) + let points = createInstancePoints(data.points); let datasets = []; datasets.push(createMainLineDataset(data, points, lineColor, bgColor)); if (points.length > 0 && widget.options.drawAvgLine) { @@ -160,12 +160,12 @@ var MonitoringConsoleRender = (function() { } return { - chart: function(update) { - var data = update.data; - var widget = update.widget; - var chart = update.chart(); - var datasets = []; - for (var j = 0; j < data.length; j++) { + onDataUpdate: function(update) { + let data = update.data; + let widget = update.widget; + let chart = update.chart(); + let datasets = []; + for (let j = 0; j < data.length; j++) { datasets = datasets.concat( createInstanceDatasets(widget, data[j], DEFAULT_LINE_COLORS[j], DEFAULT_BG_COLORS[j])); } diff --git a/appserver/monitoring-console/webapp/src/main/webapp/mc-main.js b/appserver/monitoring-console/webapp/src/main/webapp/mc-main.js new file mode 100644 index 00000000000..5094ed644c9 --- /dev/null +++ b/appserver/monitoring-console/webapp/src/main/webapp/mc-main.js @@ -0,0 +1,61 @@ +/* + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2019 Payara Foundation and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://github.com/payara/Payara/blob/master/LICENSE.txt + See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at glassfish/legal/LICENSE.txt. + + GPL Classpath Exception: + The Payara Foundation designates this particular file as subject to the "Classpath" + exception as provided by the Payara Foundation in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. +*/ + +/*jshint esversion: 8 */ + +Chart.defaults.global.defaultFontColor = "#fff"; + +/** + * The different parts of the Monitoring Console are added as the below properties by the individual files. + */ +var MonitoringConsole = { + /** + * Functions to update the actual HTML page of the MC + **/ + View: undefined, + /** + * Functions of manipulate the model of the MC (often returns a layout that is applied to the View) + **/ + Model: undefined, + /** + * Functions specifically to take the data and prepare the display of a line chart using the underlying charting library. + **/ + LineChart: undefined, +}; diff --git a/appserver/monitoring-console/webapp/src/main/webapp/mc-ui.js b/appserver/monitoring-console/webapp/src/main/webapp/mc-model.js similarity index 72% rename from appserver/monitoring-console/webapp/src/main/webapp/mc-ui.js rename to appserver/monitoring-console/webapp/src/main/webapp/mc-model.js index 8154041062f..1c0a5b245b4 100644 --- a/appserver/monitoring-console/webapp/src/main/webapp/mc-ui.js +++ b/appserver/monitoring-console/webapp/src/main/webapp/mc-model.js @@ -40,71 +40,12 @@ /*jshint esversion: 8 */ -Chart.defaults.global.defaultFontColor = "#fff"; - -/** - * A utility with 'static' helper functions that have no side effect. - * - * Extracting such function into this object should help organise the code and allow context independent testing - * of the helper functions in the browser. - * - * The MonitoringConsole object is dependent on this object but not vice versa. - */ -var MonitoringConsoleUtils = (function() { - - return { - - getSpan: function(widget, numberOfColumns, currentColumn) { - let span = widget.grid && widget.grid.span ? widget.grid.span : 1; - if (typeof span === 'string') { - if (span === 'full') { - span = numberOfColumns; - } else { - span = parseInt(span); - } - } - if (span > numberOfColumns - currentColumn) { - span = numberOfColumns - currentColumn; - } - return span; - }, - - getPageId: function(name) { - return name.replace(/[^-a-zA-Z0-9]/g, '_').toLowerCase(); - }, - - getTimeLabel: function(value, index, values) { - if (values.length == 0 || index == 0) - return value; - let span = values[values.length -1].value - values[0].value; - if (span < 120000) { // less then two minutes - let lastMinute = new Date(values[index-1].value).getMinutes(); - return new Date(values[index].value).getMinutes() != lastMinute ? value : ''+new Date(values[index].value).getSeconds(); - } - return value; - }, - - readTextFile: function(file) { - return new Promise(function(resolve, reject){ - var reader = new FileReader(); - reader.onload = function(evt){ - resolve(evt.target.result); - }; - reader.onerror = function(err) { - reject(err); - }; - reader.readAsText(file); - }); - }, - }; -})(); - /** * The object that manages the internal state of the monitoring console page. * - * It depends on the MonitoringConsoleUtils object. + * It depends on the MonitoringConsole.Utils object. */ -var MonitoringConsole = (function() { +MonitoringConsole.Model = (function() { /** * Key used in local stage for the userInterface */ @@ -127,6 +68,10 @@ var MonitoringConsole = (function() { settings: {}, }; + function getPageId(name) { + return name.replace(/[^-a-zA-Z0-9]/g, '_').toLowerCase(); + } + /** * Internal API for managing set model of the user interface. @@ -156,7 +101,7 @@ var MonitoringConsole = (function() { */ function sanityCheckPage(page) { if (!page.id) - page.id = MonitoringConsoleUtils.getPageId(page.name); + page.id = getPageId(page.name); if (!page.widgets) page.widgets = {}; if (!page.numberOfColumns || page.numberOfColumns < 1) @@ -203,7 +148,7 @@ var MonitoringConsole = (function() { function doCreate(name) { if (!name) throw "New page must have a unique name"; - var id = MonitoringConsoleUtils.getPageId(name); + var id = getPageId(name); if (pages[id]) throw "A page with name "+name+" already exist"; let page = sanityCheckPage({name: name}); @@ -249,6 +194,106 @@ var MonitoringConsole = (function() { let ui = { pages: pages, settings: settings }; return prettyPrint ? JSON.stringify(ui, null, 2) : JSON.stringify(ui); } + + function readTextFile(file) { + return new Promise(function(resolve, reject) { + let reader = new FileReader(); + reader.onload = function(evt){ + resolve(evt.target.result); + }; + reader.onerror = function(err) { + reject(err); + }; + reader.readAsText(file); + }); + } + + function doLayout(columns) { + let page = pages[currentPageId]; + if (!page) + return []; + if (columns) + page.numberOfColumns = columns; + let numberOfColumns = page.numberOfColumns || 1; + let widgets = page.widgets; + // init temporary and result data structure + let widgetsByColumn = new Array(numberOfColumns); + var layout = new Array(numberOfColumns); + for (let col = 0; col < numberOfColumns; col++) { + widgetsByColumn[col] = []; + layout[col] = []; + } + // organise widgets in columns + Object.values(widgets).forEach(function(widget) { + let column = widget.grid && widget.grid.column ? widget.grid.column : 0; + widgetsByColumn[Math.min(Math.max(column, 0), widgetsByColumn.length - 1)].push(widget); + }); + // order columns by item position + for (let col = 0; col < numberOfColumns; col++) { + widgetsByColumn[col] = widgetsByColumn[col].sort(function (a, b) { + if (!a.grid || !a.grid.item) + return -1; + if (!b.grid || !b.grid.item) + return 1; + return a.grid.item - b.grid.item; + }); + } + // do layout by marking cells with item (left top corner in case of span), null (empty) and undefined (spanned) + for (let col = 0; col < numberOfColumns; col++) { + let columnWidgets = widgetsByColumn[col]; + for (let item = 0; item < columnWidgets.length; item++) { + let widget = columnWidgets[item]; + let span = getSpan(widget, numberOfColumns, col); + let info = { span: span, widget: widget}; + let column0 = layout[col]; + let row0 = getEmptyRowIndex(column0, span); + for (let spanX = 0; spanX < span; spanX++) { + let column = layout[col + spanX]; + if (spanX == 0) { + if (!widget.grid) + widget.grid = { column: col, span: span }; // init grid + widget.grid.item = column.length; // update item position + } else { + while (column.length < row0) + column.push(null); // null marks empty cells + } + for (let spanY = 0; spanY < span; spanY++) { + column.push(spanX === 0 && spanY === 0 ? info : undefined); + } + } + } + } + // give the layout a uniform row number + let maxRows = Math.max(numberOfColumns, layout.map(column => column.length).reduce((acc, cur) => acc ? Math.max(acc, cur) : cur)); + for (let col = 0; col < numberOfColumns; col++) { + while (layout[col].length < maxRows) { + layout[col].push(null); + } + } + return layout; + } + + function getSpan(widget, numberOfColumns, currentColumn) { + let span = widget.grid && widget.grid.span ? widget.grid.span : 1; + if (typeof span === 'string') { + if (span === 'full') { + span = numberOfColumns; + } else { + span = parseInt(span); + } + } + if (span > numberOfColumns - currentColumn) { + span = numberOfColumns - currentColumn; + } + return span; + } + + /** + * @return {number} row position in column where n rows are still empty ('null' marks empty) + */ + function getEmptyRowIndex(column, n) { + return Math.max(column.length, column.findIndex((elem,index,array) => array.slice(index, index + n).every(e => e === null))); + } return { currentPage: function() { @@ -261,7 +306,7 @@ var MonitoringConsole = (function() { }); }, - $export: function() { + exportPages: function() { return doExport(true); }, @@ -269,11 +314,11 @@ var MonitoringConsole = (function() { * @param {FileList|object} userInterface - a plain user interface configuration object or a file containing such an object * @param {function} onImportComplete - optional function to call when import is done */ - $import: async (userInterface, onImportComplete) => { + importPages: async (userInterface, onImportComplete) => { if (userInterface instanceof FileList) { let file = userInterface[0]; if (file) { - let json = await MonitoringConsoleUtils.readTextFile(file); + let json = await readTextFile(file); doImport(JSON.parse(json)); } } else { @@ -302,9 +347,9 @@ var MonitoringConsole = (function() { }, renamePage: function(name) { - let pageId = MonitoringConsoleUtils.getPageId(name); + let pageId = getPageId(name); if (pages[pageId]) - throw "Page with name already exist"; + return false; let page = pages[currentPageId]; page.name = name; page.id = pageId; @@ -312,6 +357,7 @@ var MonitoringConsole = (function() { delete pages[currentPageId]; currentPageId = pageId; doStore(); + return true; }, /** @@ -390,49 +436,7 @@ var MonitoringConsole = (function() { }, arrange: function(columns) { - let page = pages[currentPageId]; - if (!page) - return []; - if (columns) - page.numberOfColumns = columns; - let numberOfColumns = page.numberOfColumns || 1; - let widgets = page.widgets; - let configsByColumn = new Array(numberOfColumns); - for (let col = 0; col < numberOfColumns; col++) - configsByColumn[col] = []; - // insert order widgets - Object.values(widgets).forEach(function(widget) { - let column = widget.grid && widget.grid.column ? widget.grid.column : 0; - configsByColumn[Math.min(Math.max(column, 0), configsByColumn.length - 1)].push(widget); - }); - // build up rows with columns, occupy spans with empty - var layout = new Array(numberOfColumns); - for (let col = 0; col < numberOfColumns; col++) - layout[col] = []; - for (let col = 0; col < numberOfColumns; col++) { - let orderedConfigs = configsByColumn[col].sort(function (a, b) { - if (!a.grid || !a.grid.item) - return -1; - if (!b.grid || !b.grid.item) - return 1; - return a.grid.item - b.grid.item; - }); - orderedConfigs.forEach(function(widget) { - let span = MonitoringConsoleUtils.getSpan(widget, numberOfColumns, col); - let info = { span: span, widget: widget}; - for (let spanX = 0; spanX < span; spanX++) { - let column = layout[col + spanX]; - if (spanX == 0) { - if (!widget.grid) - widget.grid = { column: col, span: span }; // init grid - widget.grid.item = column.length; // update item position - } - for (let spanY = 0; spanY < span; spanY++) { - column.push(spanX === 0 && spanY === 0 ? info : null); - } - } - }); - } + let layout = doLayout(columns); doStore(); return layout; }, @@ -492,6 +496,17 @@ var MonitoringConsole = (function() { options.scales.xAxes[0].ticks.maxRotation = rotation; options.legend.display = widget.options.showLegend === true; } + + function getTimeLabel(value, index, values) { + if (values.length == 0 || index == 0) + return value; + let span = values[values.length -1].value - values[0].value; + if (span < 120000) { // less then two minutes + let lastMinute = new Date(values[index-1].value).getMinutes(); + return new Date(values[index].value).getMinutes() != lastMinute ? value : ''+new Date(values[index].value).getSeconds(); + } + return value; + } return { /** @@ -520,7 +535,7 @@ var MonitoringConsole = (function() { round: 'second', }, ticks: { - callback: MonitoringConsoleUtils.getTimeLabel, + callback: getTimeLabel, minRotation: 90, maxRotation: 90, } @@ -633,63 +648,75 @@ var MonitoringConsole = (function() { }; })(); - return { - - init: function(onDataUpdate) { - UI.load(); - Interval.init(function() { - let widgets = UI.currentPage().widgets; - let payload = { - }; - let instances = $('#cfgInstances').val(); - payload.series = Object.keys(widgets).map(function(series) { - return { - series: series, - instances: instances - }; - }); - let request = $.ajax({ - url: 'api/series/data/', - type: 'POST', - data: JSON.stringify(payload), - contentType:"application/json; charset=utf-8", - dataType:"json", - }); - request.done(function(response) { - Object.values(widgets).forEach(function(widget) { - onDataUpdate({ - widget: widget, - data: response[widget.series], - chart: () => Charts.getOrCreate(widget), - }); + function doInit(onDataUpdate) { + UI.load(); + Interval.init(function() { + let widgets = UI.currentPage().widgets; + let payload = { + }; + let instances = $('#cfgInstances').val(); + payload.series = Object.keys(widgets).map(function(series) { + return { + series: series, + instances: instances + }; + }); + let request = $.ajax({ + url: 'api/series/data/', + type: 'POST', + data: JSON.stringify(payload), + contentType:"application/json; charset=utf-8", + dataType:"json", + }); + request.done(function(response) { + Object.values(widgets).forEach(function(widget) { + onDataUpdate({ + widget: widget, + data: response[widget.series], + chart: () => Charts.getOrCreate(widget), }); }); - request.fail(function(jqXHR, textStatus) { - Object.values(widgets).forEach(function(widget) { - onDataUpdate({ - widget: widget, - chart: () => Charts.getOrCreate(widget), - }); + }); + request.fail(function(jqXHR, textStatus) { + Object.values(widgets).forEach(function(widget) { + onDataUpdate({ + widget: widget, + chart: () => Charts.getOrCreate(widget), }); }); }); - Interval.resume(); - return UI.arrange(); - }, + }); + Interval.resume(); + return UI.arrange(); + } + + function doConfigureSelection(widgetUpdate) { + UI.configureWidget(widgetUpdate).forEach(Charts.update); + return UI.arrange(); + } + + function doConfigureWidget(series, widgetUpdate) { + UI.configureWidget(widgetUpdate, series).forEach(Charts.update); + return UI.arrange(); + } + + /** + * The public API object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + return { - $export: UI.$export, - $import: function(userInterface, onImportComplete) { - UI.$import(userInterface, () => onImportComplete(UI.arrange())); - }, + init: doInit, /** * @param {function} consumer - a function with one argument accepting the array of series names */ - listSeries: function(consumer) { - $.getJSON("api/series/", consumer); - }, - + listSeries: (consumer) => $.getJSON("api/series/", consumer), + listPages: UI.listPages, + exportPages: UI.exportPages, + importPages: function(userInterface, onImportComplete) { + UI.importPages(userInterface, () => onImportComplete(UI.arrange())); + }, /** * API to control the chart refresh interval. @@ -770,10 +797,33 @@ var MonitoringConsole = (function() { return UI.arrange(); }, - configure: function(series, widgetUpdate) { - UI.configureWidget(widgetUpdate, series).forEach(Charts.update); - return UI.arrange(); - }, + configure: doConfigureWidget, + + moveLeft: (series) => doConfigureWidget(series, function(widget) { + if (!widget.grid.column || widget.grid.column > 0) { + widget.grid.item = undefined; + widget.grid.column = widget.grid.column ? widget.grid.column - 1 : 1; + } + }), + + moveRight: (series) => doConfigureWidget(series, function(widget) { + if (!widget.grid.column || widget.grid.column < 4) { + widget.grid.item = undefined; + widget.grid.column = widget.grid.column ? widget.grid.column + 1 : 1; + } + }), + + spanMore: (series) => doConfigureWidget(series, function(widget) { + if (! widget.grid.span || widget.grid.span < 4) { + widget.grid.span = !widget.grid.span ? 2 : widget.grid.span + 1; + } + }), + + spanLess: (series) => doConfigureWidget(series, function(widget) { + if (widget.grid.span > 1) { + widget.grid.span -= 1; + } + }), /** * API for the set of selected widgets on the current page. @@ -781,16 +831,16 @@ var MonitoringConsole = (function() { Selection: { listSeries: UI.selected, + isSingle: () => UI.selected().length == 1, + first: () => UI.currentPage().widgets[UI.selected()[0]], toggle: UI.select, clear: UI.deselect, /** * @param {function} widgetUpdate - a function accepting chart configuration applied to each chart */ - configure: function(widgetUpdate) { - UI.configureWidget(widgetUpdate).forEach(Charts.update); - return UI.arrange(); - }, + configure: doConfigureSelection, + }, }, diff --git a/appserver/monitoring-console/webapp/src/main/webapp/mc-page.js b/appserver/monitoring-console/webapp/src/main/webapp/mc-page.js deleted file mode 100644 index 79a44b4e6eb..00000000000 --- a/appserver/monitoring-console/webapp/src/main/webapp/mc-page.js +++ /dev/null @@ -1,399 +0,0 @@ -/* - DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - - Copyright (c) 2019 Payara Foundation and/or its affiliates. All rights reserved. - - The contents of this file are subject to the terms of either the GNU - General Public License Version 2 only ("GPL") or the Common Development - and Distribution License("CDDL") (collectively, the "License"). You - may not use this file except in compliance with the License. You can - obtain a copy of the License at - https://github.com/payara/Payara/blob/master/LICENSE.txt - See the License for the specific - language governing permissions and limitations under the License. - - When distributing the software, include this License Header Notice in each - file and include the License file at glassfish/legal/LICENSE.txt. - - GPL Classpath Exception: - The Payara Foundation designates this particular file as subject to the "Classpath" - exception as provided by the Payara Foundation in the GPL Version 2 section of the License - file that accompanied this code. - - Modifications: - If applicable, add the following below the License Header, with the fields - enclosed by brackets [] replaced by your own identifying information: - "Portions Copyright [year] [name of copyright owner]" - - Contributor(s): - If you wish your version of this file to be governed by only the CDDL or - only the GPL Version 2, indicate your decision by adding "[Contributor] - elects to include this software in this distribution under the [CDDL or GPL - Version 2] license." If you don't indicate a single choice of license, a - recipient has the option to distribute your version of this file under - either the CDDL, the GPL Version 2 or to extend the choice of license to - its licensees as provided above. However, if you add GPL Version 2 code - and therefore, elected the GPL Version 2 license, then the option applies - only if the new code is made subject to such option by the copyright - holder. -*/ - -/*jshint esversion: 8 */ - -var MonitoringConsolePage = (function() { - - function download(filename, text) { - var pom = document.createElement('a'); - pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); - pom.setAttribute('download', filename); - - if (document.createEvent) { - var event = document.createEvent('MouseEvents'); - event.initEvent('click', true, true); - pom.dispatchEvent(event); - } - else { - pom.click(); - } - } - - function renderPageTabs() { - var bar = $("#pagesTabs"); - bar.empty(); - MonitoringConsole.listPages().forEach(function(page) { - var tabId = page.id + '-tab'; - var css = "page-tab" + (page.active ? ' page-selected' : ''); - //TODO when clicking active page tab the options could open/close - var newTab = $('', {id: tabId, "class": css, text: page.name}); - newTab.click(function() { - onPageChange(MonitoringConsole.Page.changeTo(page.id)); - }); - bar.append(newTab); - }); - var addPage = $('', {id: 'addPageTab', 'class': 'page-tab'}).html('+'); - addPage.click(function() { - onPageChange(MonitoringConsole.Page.create('(Unnamed)')); - }); - bar.append(addPage); - } - - /** - * Each chart needs to be in a relative positioned box to allow responsive sizing. - * This fuction creates this box including the canvas element the chart is drawn upon. - */ - function renderChartBox(cell) { - var boxId = cell.widget.target + '-box'; - var box = $('#'+boxId); - if (box.length > 0) - return box.first(); - box = $('
', { id: boxId, "class": "chart-box" }); - var win = $(window); - box.append($('',{ id: cell.widget.target })); - return box; - } - - /** - * This function refleshes the page with the given layout. - */ - function renderPage(layout) { - var table = $("", { id: 'chart-grid'}); - var numberOfColumns = layout.length; - var maxRow = 0; - for (var col = 0; col < numberOfColumns; col++) { - maxRow = Math.max(maxRow, layout[col].length); - } - var rowHeight = Math.round(($(window).height() - 100) / numberOfColumns); - for (var row = 0; row < maxRow; row++) { - var tr = $(""); - for (var col = 0; col < numberOfColumns; col++) { - if (layout[col].length <= row) { - tr.append($("').append($('
")); - } else { - var cell = layout[col][row]; - if (cell) { - var span = cell.span; - var td = $("", { colspan: span, rowspan: span, 'class': 'box', style: 'height: '+(span * rowHeight)+"px;"}); - td.append(renderChartCaption(cell)); - var status = $('
', { "class": 'status-nodata'}); - status.append($('
', {text: 'No Data'})); - td.append(status); - td.append(renderChartBox(cell)); - tr.append(td); - } else if (row == 0 && layout[col].length == 0) { - // a column with no content, span all rows - var td = $("
", { rowspan: maxRow }); - tr.append(td); - } - } - } - table.append(tr); - } - $('#chart-container').empty(); - $('#chart-container').append(table); - } - - function camelCaseToWords(str) { - return str.replace(/([A-Z]+)/g, " $1").replace(/([A-Z][a-z])/g, " $1"); - } - - function renderChartCaption(cell) { - var bar = $('
', {"class": "caption-bar"}); - var series = cell.widget.series; - var endOfTags = series.lastIndexOf(' '); - var text = endOfTags <= 0 - ? camelCaseToWords(series) - : ''+series.substring(0, endOfTags)+' '+camelCaseToWords(series.substring(endOfTags + 1)); - if (cell.widget.options.perSec) { - text += ' (per second)'; - } - var caption = $('

', {title: 'Select '+series}).html(text); - caption.click(function() { - if (MonitoringConsole.Page.Widgets.Selection.toggle(series)) { - bar.parent().addClass('chart-selected'); - } else { - bar.parent().removeClass('chart-selected'); - } - renderChartOptions(); - }); - bar.append(caption); - var btnClose = $('

', {colspan: 2, text: caption}).click(function() { + let tr = $(this).closest('tr').next(); + while (tr.length > 0 && !tr.children('th').length > 0) { + tr.children().toggle(); + tr = tr.next(); + } + })); + } + + function createSettingsCheckboxRow(label, checked, onChange) { + return createSettingsRow(label, () => createConfigurationCheckbox(checked, onChange)); + } + + function createSettingsTable(id) { + return $('', { 'class': 'settings', id: id }); + } + + function createSettingsRow(label, createInput) { + return $('').append($('
').text(label)).append($('').append(createInput())); + } + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~[ Event Handlers ]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + function onWidgetToolbarClick(widget) { + MonitoringConsole.Model.Page.Widgets.Selection.toggle(widget.series); + onWidgetUpdate(widget); + updatePageAndSelectionSettings(); + } + + function onWidgetDelete() { + if (window.confirm('Do you really want to remove the chart from the page?')) { + onPageUpdate(MonitoringConsole.Model.Page.Widgets.remove(series)); + } + } + + function onPageExport(filename, text) { + let pom = document.createElement('a'); + pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); + pom.setAttribute('download', filename); + + if (document.createEvent) { + let event = document.createEvent('MouseEvents'); + event.initEvent('click', true, true); + pom.dispatchEvent(event); + } + else { + pom.click(); + } + } + + /** + * This function is called when data was received or was failed to receive so the new data can be applied to the page. + * + * Depending on the update different content is rendered within a chart box. + */ + function onDataUpdate(update) { + let boxId = update.widget.target + '-box'; + let box = $('#'+boxId); + if (box.length == 0) { + if (console && console.log) + console.log('WARN: Box for chart ' + update.widget.series + ' not ready.'); + return; + } + let td = box.closest('.widget'); + if (update.data) { + td.children('.status-nodata').hide(); + let points = update.data[0].points; + if (points.length == 4 && points[1] === points[3] && !update.widget.options.perSec) { + if (td.children('.stable').length == 0) { + let info = $('
', { 'class': 'stable' }); + info.append($('', { text: points[1] })); + td.append(info); + box.hide(); + } + } else { + td.children('.stable').remove(); + box.show(); + MonitoringConsole.LineChart.onDataUpdate(update); + } + } else { + td.children('.status-nodata').width(box.width()-10).height(box.height()-10).show(); + } + + onWidgetUpdate(update.widget); + } + + /** + * Called when changes to the widget require to update the view of the widget (non data related changes) + */ + function onWidgetUpdate(widget) { + let container = $('#' + widget.target + '-box').closest('.widget'); + if (widget.selected) { + container.addClass('chart-selected'); + } else { + container.removeClass('chart-selected'); + } + } + + /** + * This function refleshes the page with the given layout. + */ + function onPageUpdate(layout) { + let numberOfColumns = layout.length; + let maxRows = layout[0].length; + let table = $("", { id: 'chart-grid', 'class': 'columns-'+numberOfColumns + ' rows-'+maxRows }); + let rowHeight = Math.round(($(window).height() - 100) / numberOfColumns); + for (let row = 0; row < maxRows; row++) { + let tr = $(""); + for (let col = 0; col < numberOfColumns; col++) { + let cell = layout[col][row]; + if (cell) { + let span = cell.span; + let td = $("
", { colspan: span, rowspan: span, 'class': 'widget', style: 'height: '+(span * rowHeight)+"px;"}); + td.append(createWidgetToolbar(cell)); + let status = $('
', { "class": 'status-nodata'}); + status.append($('
', {text: 'No Data'})); + td.append(status); + td.append(createWidgetTargetContainer(cell)); + tr.append(td); + } else if (cell === null) { + tr.append($("
", { 'class': 'widget', style: 'height: '+rowHeight+'px;'})); + } + } + table.append(tr); + } + $('#chart-container').empty(); + $('#chart-container').append(table); + } + + /** + * Method to call when page changes to update UI elements accordingly + */ + function onPageChange(layout) { + onPageUpdate(layout); + updatePageNavigation(); + updatePageAndSelectionSettings(); + } + + /** + * Public API of the View object: + */ + return { + onPageReady: function() { + // connect the view to the model by passing the 'onDataUpdate' function to the model + // which will call it when data is received + onPageUpdate(MonitoringConsole.Model.init(onDataUpdate)); + updatePageAndSelectionSettings(); + updatePageNavigation(); + }, + onPageChange: (layout) => onPageChange(layout), + onPageUpdate: (layout) => onPageUpdate(layout), + onPageReset: () => onPageChange(MonitoringConsole.Model.Page.reset()), + onPageImport: () => MonitoringConsole.Model.importPages(this.files, onPageChange), + onPageExport: () => onPageExport('monitoring-console-config.json', MonitoringConsole.Model.exportPages()), + onPageMenu: function() { MonitoringConsole.Model.Settings.toggle(); updatePageAndSelectionSettings(); }, + onPageLayoutChange: (numberOfColumns) => onPageUpdate(MonitoringConsole.Model.Page.arrange(numberOfColumns)), + onPageDelete: function() { + if (window.confirm("Do you really want to delete the current page?")) { + onPageUpdate(MonitoringConsole.Model.Page.erase()); + updatePageNavigation(); + } + }, + }; +})(); diff --git a/appserver/monitoring-console/webapp/src/main/webapp/monitoring-console.css b/appserver/monitoring-console/webapp/src/main/webapp/monitoring-console.css index 95710d7037c..f32ce695d7b 100644 --- a/appserver/monitoring-console/webapp/src/main/webapp/monitoring-console.css +++ b/appserver/monitoring-console/webapp/src/main/webapp/monitoring-console.css @@ -90,7 +90,6 @@ button:hover { vertical-align: top; } #chart-container { - position: relative; padding: 0 10px; } .chart-box { @@ -107,7 +106,7 @@ button:hover { background-color: transparent; font-weight: bold; } -.btnClose { +.btnIcon:first-of-type { margin-left: 15px; } button.btnIcon:hover { @@ -221,9 +220,9 @@ button.btnIcon:hover { } /** - * Properties (Sizes and Visibility) + * Widget Settings (Sizes and Visibility) */ -#panel-properties { +#panel-settings { width: 380px; padding: 0; background-image: url("payara-server-logo.png"); @@ -234,27 +233,28 @@ button.btnIcon:hover { top: 5px; right: 10px; } -.properties { +.settings { background-color: #002433; padding: 10px; margin-top: 5px; width: 100%; } -button.btnClose + .properties { +button.btnClose + .settings { margin-top: 210px; } -.properties th { +.settings th { border-bottom: 1px solid #bbb; + cursor: pointer; } -.properties td:first-child { +.settings td:first-child { width: 120px; } -#console #panel-properties { +#console #panel-settings { display: none; } -#console.state-show-properties #panel-properties { +#console.state-show-settings #panel-settings { display: block; } -#console.state-show-properties #panel-grid { - width: calc(100% - 385px); +#console.state-show-settings #panel-grid { + width: calc(100% - 385px); } diff --git a/appserver/monitoring-console/webapp/src/main/webapp/monitoring-console.js b/appserver/monitoring-console/webapp/src/main/webapp/monitoring-console.js index 23ae5bc8e6a..6b9fe2c5fc7 100644 --- a/appserver/monitoring-console/webapp/src/main/webapp/monitoring-console.js +++ b/appserver/monitoring-console/webapp/src/main/webapp/monitoring-console.js @@ -43,68 +43,70 @@ Chart.defaults.global.defaultFontColor = "#fff"; /** - * A utility with 'static' helper functions that have no side effect. - * - * Extracting such function into this object should help organise the code and allow context independent testing - * of the helper functions in the browser. - * - * The MonitoringConsole object is dependent on this object but not vice versa. + * The different parts of the Monitoring Console are added as the below properties by the individual files. */ -var MonitoringConsoleUtils = (function() { - - return { - - getSpan: function(widget, numberOfColumns, currentColumn) { - let span = widget.grid && widget.grid.span ? widget.grid.span : 1; - if (typeof span === 'string') { - if (span === 'full') { - span = numberOfColumns; - } else { - span = parseInt(span); - } - } - if (span > numberOfColumns - currentColumn) { - span = numberOfColumns - currentColumn; - } - return span; - }, - - getPageId: function(name) { - return name.replace(/[^-a-zA-Z0-9]/g, '_').toLowerCase(); - }, - - getTimeLabel: function(value, index, values) { - if (values.length == 0 || index == 0) - return value; - let span = values[values.length -1].value - values[0].value; - if (span < 120000) { // less then two minutes - let lastMinute = new Date(values[index-1].value).getMinutes(); - return new Date(values[index].value).getMinutes() != lastMinute ? value : ''+new Date(values[index].value).getSeconds(); - } - return value; - }, - - readTextFile: function(file) { - return new Promise(function(resolve, reject){ - var reader = new FileReader(); - reader.onload = function(evt){ - resolve(evt.target.result); - }; - reader.onerror = function(err) { - reject(err); - }; - reader.readAsText(file); - }); - }, - }; -})(); +var MonitoringConsole = { + /** + * Functions to update the actual HTML page of the MC + **/ + View: undefined, + /** + * Functions of manipulate the model of the MC (often returns a layout that is applied to the View) + **/ + Model: undefined, + /** + * Functions specifically to take the data and prepare the display of a line chart using the underlying charting library. + **/ + LineChart: undefined, +}; +/* + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2019 Payara Foundation and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://github.com/payara/Payara/blob/master/LICENSE.txt + See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at glassfish/legal/LICENSE.txt. + + GPL Classpath Exception: + The Payara Foundation designates this particular file as subject to the "Classpath" + exception as provided by the Payara Foundation in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. +*/ + +/*jshint esversion: 8 */ /** * The object that manages the internal state of the monitoring console page. * - * It depends on the MonitoringConsoleUtils object. + * It depends on the MonitoringConsole.Utils object. */ -var MonitoringConsole = (function() { +MonitoringConsole.Model = (function() { /** * Key used in local stage for the userInterface */ @@ -127,6 +129,10 @@ var MonitoringConsole = (function() { settings: {}, }; + function getPageId(name) { + return name.replace(/[^-a-zA-Z0-9]/g, '_').toLowerCase(); + } + /** * Internal API for managing set model of the user interface. @@ -156,7 +162,7 @@ var MonitoringConsole = (function() { */ function sanityCheckPage(page) { if (!page.id) - page.id = MonitoringConsoleUtils.getPageId(page.name); + page.id = getPageId(page.name); if (!page.widgets) page.widgets = {}; if (!page.numberOfColumns || page.numberOfColumns < 1) @@ -203,7 +209,7 @@ var MonitoringConsole = (function() { function doCreate(name) { if (!name) throw "New page must have a unique name"; - var id = MonitoringConsoleUtils.getPageId(name); + var id = getPageId(name); if (pages[id]) throw "A page with name "+name+" already exist"; let page = sanityCheckPage({name: name}); @@ -249,6 +255,106 @@ var MonitoringConsole = (function() { let ui = { pages: pages, settings: settings }; return prettyPrint ? JSON.stringify(ui, null, 2) : JSON.stringify(ui); } + + function readTextFile(file) { + return new Promise(function(resolve, reject) { + let reader = new FileReader(); + reader.onload = function(evt){ + resolve(evt.target.result); + }; + reader.onerror = function(err) { + reject(err); + }; + reader.readAsText(file); + }); + } + + function doLayout(columns) { + let page = pages[currentPageId]; + if (!page) + return []; + if (columns) + page.numberOfColumns = columns; + let numberOfColumns = page.numberOfColumns || 1; + let widgets = page.widgets; + // init temporary and result data structure + let widgetsByColumn = new Array(numberOfColumns); + var layout = new Array(numberOfColumns); + for (let col = 0; col < numberOfColumns; col++) { + widgetsByColumn[col] = []; + layout[col] = []; + } + // organise widgets in columns + Object.values(widgets).forEach(function(widget) { + let column = widget.grid && widget.grid.column ? widget.grid.column : 0; + widgetsByColumn[Math.min(Math.max(column, 0), widgetsByColumn.length - 1)].push(widget); + }); + // order columns by item position + for (let col = 0; col < numberOfColumns; col++) { + widgetsByColumn[col] = widgetsByColumn[col].sort(function (a, b) { + if (!a.grid || !a.grid.item) + return -1; + if (!b.grid || !b.grid.item) + return 1; + return a.grid.item - b.grid.item; + }); + } + // do layout by marking cells with item (left top corner in case of span), null (empty) and undefined (spanned) + for (let col = 0; col < numberOfColumns; col++) { + let columnWidgets = widgetsByColumn[col]; + for (let item = 0; item < columnWidgets.length; item++) { + let widget = columnWidgets[item]; + let span = getSpan(widget, numberOfColumns, col); + let info = { span: span, widget: widget}; + let column0 = layout[col]; + let row0 = getEmptyRowIndex(column0, span); + for (let spanX = 0; spanX < span; spanX++) { + let column = layout[col + spanX]; + if (spanX == 0) { + if (!widget.grid) + widget.grid = { column: col, span: span }; // init grid + widget.grid.item = column.length; // update item position + } else { + while (column.length < row0) + column.push(null); // null marks empty cells + } + for (let spanY = 0; spanY < span; spanY++) { + column.push(spanX === 0 && spanY === 0 ? info : undefined); + } + } + } + } + // give the layout a uniform row number + let maxRows = Math.max(numberOfColumns, layout.map(column => column.length).reduce((acc, cur) => acc ? Math.max(acc, cur) : cur)); + for (let col = 0; col < numberOfColumns; col++) { + while (layout[col].length < maxRows) { + layout[col].push(null); + } + } + return layout; + } + + function getSpan(widget, numberOfColumns, currentColumn) { + let span = widget.grid && widget.grid.span ? widget.grid.span : 1; + if (typeof span === 'string') { + if (span === 'full') { + span = numberOfColumns; + } else { + span = parseInt(span); + } + } + if (span > numberOfColumns - currentColumn) { + span = numberOfColumns - currentColumn; + } + return span; + } + + /** + * @return {number} row position in column where n rows are still empty ('null' marks empty) + */ + function getEmptyRowIndex(column, n) { + return Math.max(column.length, column.findIndex((elem,index,array) => array.slice(index, index + n).every(e => e === null))); + } return { currentPage: function() { @@ -261,7 +367,7 @@ var MonitoringConsole = (function() { }); }, - $export: function() { + exportPages: function() { return doExport(true); }, @@ -269,11 +375,11 @@ var MonitoringConsole = (function() { * @param {FileList|object} userInterface - a plain user interface configuration object or a file containing such an object * @param {function} onImportComplete - optional function to call when import is done */ - $import: async (userInterface, onImportComplete) => { + importPages: async (userInterface, onImportComplete) => { if (userInterface instanceof FileList) { let file = userInterface[0]; if (file) { - let json = await MonitoringConsoleUtils.readTextFile(file); + let json = await readTextFile(file); doImport(JSON.parse(json)); } } else { @@ -302,9 +408,9 @@ var MonitoringConsole = (function() { }, renamePage: function(name) { - let pageId = MonitoringConsoleUtils.getPageId(name); + let pageId = getPageId(name); if (pages[pageId]) - throw "Page with name already exist"; + return false; let page = pages[currentPageId]; page.name = name; page.id = pageId; @@ -312,6 +418,7 @@ var MonitoringConsole = (function() { delete pages[currentPageId]; currentPageId = pageId; doStore(); + return true; }, /** @@ -390,49 +497,7 @@ var MonitoringConsole = (function() { }, arrange: function(columns) { - let page = pages[currentPageId]; - if (!page) - return []; - if (columns) - page.numberOfColumns = columns; - let numberOfColumns = page.numberOfColumns || 1; - let widgets = page.widgets; - let configsByColumn = new Array(numberOfColumns); - for (let col = 0; col < numberOfColumns; col++) - configsByColumn[col] = []; - // insert order widgets - Object.values(widgets).forEach(function(widget) { - let column = widget.grid && widget.grid.column ? widget.grid.column : 0; - configsByColumn[Math.min(Math.max(column, 0), configsByColumn.length - 1)].push(widget); - }); - // build up rows with columns, occupy spans with empty - var layout = new Array(numberOfColumns); - for (let col = 0; col < numberOfColumns; col++) - layout[col] = []; - for (let col = 0; col < numberOfColumns; col++) { - let orderedConfigs = configsByColumn[col].sort(function (a, b) { - if (!a.grid || !a.grid.item) - return -1; - if (!b.grid || !b.grid.item) - return 1; - return a.grid.item - b.grid.item; - }); - orderedConfigs.forEach(function(widget) { - let span = MonitoringConsoleUtils.getSpan(widget, numberOfColumns, col); - let info = { span: span, widget: widget}; - for (let spanX = 0; spanX < span; spanX++) { - let column = layout[col + spanX]; - if (spanX == 0) { - if (!widget.grid) - widget.grid = { column: col, span: span }; // init grid - widget.grid.item = column.length; // update item position - } - for (let spanY = 0; spanY < span; spanY++) { - column.push(spanX === 0 && spanY === 0 ? info : null); - } - } - }); - } + let layout = doLayout(columns); doStore(); return layout; }, @@ -492,6 +557,17 @@ var MonitoringConsole = (function() { options.scales.xAxes[0].ticks.maxRotation = rotation; options.legend.display = widget.options.showLegend === true; } + + function getTimeLabel(value, index, values) { + if (values.length == 0 || index == 0) + return value; + let span = values[values.length -1].value - values[0].value; + if (span < 120000) { // less then two minutes + let lastMinute = new Date(values[index-1].value).getMinutes(); + return new Date(values[index].value).getMinutes() != lastMinute ? value : ''+new Date(values[index].value).getSeconds(); + } + return value; + } return { /** @@ -520,7 +596,7 @@ var MonitoringConsole = (function() { round: 'second', }, ticks: { - callback: MonitoringConsoleUtils.getTimeLabel, + callback: getTimeLabel, minRotation: 90, maxRotation: 90, } @@ -633,63 +709,75 @@ var MonitoringConsole = (function() { }; })(); - return { - - init: function(onDataUpdate) { - UI.load(); - Interval.init(function() { - let widgets = UI.currentPage().widgets; - let payload = { - }; - let instances = $('#cfgInstances').val(); - payload.series = Object.keys(widgets).map(function(series) { - return { - series: series, - instances: instances - }; - }); - let request = $.ajax({ - url: 'api/series/data/', - type: 'POST', - data: JSON.stringify(payload), - contentType:"application/json; charset=utf-8", - dataType:"json", - }); - request.done(function(response) { - Object.values(widgets).forEach(function(widget) { - onDataUpdate({ - widget: widget, - data: response[widget.series], - chart: () => Charts.getOrCreate(widget), - }); + function doInit(onDataUpdate) { + UI.load(); + Interval.init(function() { + let widgets = UI.currentPage().widgets; + let payload = { + }; + let instances = $('#cfgInstances').val(); + payload.series = Object.keys(widgets).map(function(series) { + return { + series: series, + instances: instances + }; + }); + let request = $.ajax({ + url: 'api/series/data/', + type: 'POST', + data: JSON.stringify(payload), + contentType:"application/json; charset=utf-8", + dataType:"json", + }); + request.done(function(response) { + Object.values(widgets).forEach(function(widget) { + onDataUpdate({ + widget: widget, + data: response[widget.series], + chart: () => Charts.getOrCreate(widget), }); }); - request.fail(function(jqXHR, textStatus) { - Object.values(widgets).forEach(function(widget) { - onDataUpdate({ - widget: widget, - chart: () => Charts.getOrCreate(widget), - }); + }); + request.fail(function(jqXHR, textStatus) { + Object.values(widgets).forEach(function(widget) { + onDataUpdate({ + widget: widget, + chart: () => Charts.getOrCreate(widget), }); }); }); - Interval.resume(); - return UI.arrange(); - }, + }); + Interval.resume(); + return UI.arrange(); + } + + function doConfigureSelection(widgetUpdate) { + UI.configureWidget(widgetUpdate).forEach(Charts.update); + return UI.arrange(); + } + + function doConfigureWidget(series, widgetUpdate) { + UI.configureWidget(widgetUpdate, series).forEach(Charts.update); + return UI.arrange(); + } + + /** + * The public API object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + return { - $export: UI.$export, - $import: function(userInterface, onImportComplete) { - UI.$import(userInterface, () => onImportComplete(UI.arrange())); - }, + init: doInit, /** * @param {function} consumer - a function with one argument accepting the array of series names */ - listSeries: function(consumer) { - $.getJSON("api/series/", consumer); - }, - + listSeries: (consumer) => $.getJSON("api/series/", consumer), + listPages: UI.listPages, + exportPages: UI.exportPages, + importPages: function(userInterface, onImportComplete) { + UI.importPages(userInterface, () => onImportComplete(UI.arrange())); + }, /** * API to control the chart refresh interval. @@ -770,10 +858,33 @@ var MonitoringConsole = (function() { return UI.arrange(); }, - configure: function(series, widgetUpdate) { - UI.configureWidget(widgetUpdate, series).forEach(Charts.update); - return UI.arrange(); - }, + configure: doConfigureWidget, + + moveLeft: (series) => doConfigureWidget(series, function(widget) { + if (!widget.grid.column || widget.grid.column > 0) { + widget.grid.item = undefined; + widget.grid.column = widget.grid.column ? widget.grid.column - 1 : 1; + } + }), + + moveRight: (series) => doConfigureWidget(series, function(widget) { + if (!widget.grid.column || widget.grid.column < 4) { + widget.grid.item = undefined; + widget.grid.column = widget.grid.column ? widget.grid.column + 1 : 1; + } + }), + + spanMore: (series) => doConfigureWidget(series, function(widget) { + if (! widget.grid.span || widget.grid.span < 4) { + widget.grid.span = !widget.grid.span ? 2 : widget.grid.span + 1; + } + }), + + spanLess: (series) => doConfigureWidget(series, function(widget) { + if (widget.grid.span > 1) { + widget.grid.span -= 1; + } + }), /** * API for the set of selected widgets on the current page. @@ -781,16 +892,16 @@ var MonitoringConsole = (function() { Selection: { listSeries: UI.selected, + isSingle: () => UI.selected().length == 1, + first: () => UI.currentPage().widgets[UI.selected()[0]], toggle: UI.select, clear: UI.deselect, /** * @param {function} widgetUpdate - a function accepting chart configuration applied to each chart */ - configure: function(widgetUpdate) { - UI.configureWidget(widgetUpdate).forEach(Charts.update); - return UI.arrange(); - }, + configure: doConfigureSelection, + }, }, @@ -840,7 +951,7 @@ var MonitoringConsole = (function() { /*jshint esversion: 8 */ -var MonitoringConsoleRender = (function() { +MonitoringConsole.LineChart = (function() { const DEFAULT_BG_COLORS = [ 'rgba(153, 102, 255, 0.2)', @@ -860,8 +971,8 @@ var MonitoringConsoleRender = (function() { ]; function createMinimumLineDataset(data, points, lineColor) { - var min = data.observedMin; - var minPoints = [{t:points[0].t, y:min}, {t:points[points.length-1].t, y:min}]; + let min = data.observedMin; + let minPoints = [{t:points[0].t, y:min}, {t:points[points.length-1].t, y:min}]; return { data: minPoints, @@ -875,8 +986,8 @@ var MonitoringConsoleRender = (function() { } function createMaximumLineDataset(data, points, lineColor) { - var max = data.observedMax; - var maxPoints = [{t:points[0].t, y:max}, {t:points[points.length-1].t, y:max}]; + let max = data.observedMax; + let maxPoints = [{t:points[0].t, y:max}, {t:points[points.length-1].t, y:max}]; return { data: maxPoints, @@ -889,8 +1000,8 @@ var MonitoringConsoleRender = (function() { } function createAverageLineDataset(data, points, lineColor) { - var avg = data.observedSum / data.observedValues; - var avgPoints = [{t:points[0].t, y:avg}, {t:points[points.length-1].t, y:avg}]; + let avg = data.observedSum / data.observedValues; + let avgPoints = [{t:points[0].t, y:avg}, {t:points[points.length-1].t, y:avg}]; return { data: avgPoints, @@ -917,7 +1028,7 @@ var MonitoringConsoleRender = (function() { if (!points1d) return []; let points2d = new Array(points1d.length / 2); - for (var i = 0; i < points2d.length; i++) { + for (let i = 0; i < points2d.length; i++) { points2d[i] = { t: new Date(points1d[i*2]), y: points1d[i*2+1] }; } return points2d; @@ -927,7 +1038,7 @@ var MonitoringConsoleRender = (function() { if (!points1d) return []; let points2d = new Array((points1d.length / 2) - 1); - for (var i = 0; i < points2d.length; i++) { + for (let i = 0; i < points2d.length; i++) { let t0 = points1d[i*2]; let t1 = points1d[i*2+2]; let y0 = points1d[i*2+1]; @@ -944,7 +1055,7 @@ var MonitoringConsoleRender = (function() { if (widget.options.perSec) { return [ createMainLineDataset(data, createInstancePerSecPoints(data.points), lineColor, bgColor) ]; } - let points = createInstancePoints(data.points) + let points = createInstancePoints(data.points); let datasets = []; datasets.push(createMainLineDataset(data, points, lineColor, bgColor)); if (points.length > 0 && widget.options.drawAvgLine) { @@ -960,12 +1071,12 @@ var MonitoringConsoleRender = (function() { } return { - chart: function(update) { - var data = update.data; - var widget = update.widget; - var chart = update.chart(); - var datasets = []; - for (var j = 0; j < data.length; j++) { + onDataUpdate: function(update) { + let data = update.data; + let widget = update.widget; + let chart = update.chart(); + let datasets = []; + for (let j = 0; j < data.length; j++) { datasets = datasets.concat( createInstanceDatasets(widget, data[j], DEFAULT_LINE_COLORS[j], DEFAULT_BG_COLORS[j])); } @@ -1016,188 +1127,240 @@ var MonitoringConsoleRender = (function() { /*jshint esversion: 8 */ -var MonitoringConsolePage = (function() { - - function download(filename, text) { - var pom = document.createElement('a'); - pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); - pom.setAttribute('download', filename); +/** + * + **/ +MonitoringConsole.View = (function() { - if (document.createEvent) { - var event = document.createEvent('MouseEvents'); - event.initEvent('click', true, true); - pom.dispatchEvent(event); - } - else { - pom.click(); - } + /** + * Updates the DOM with the page navigation tabs so it reflects current model state + */ + function updatePageNavigation() { + let nav = $("#pagesTabs"); + nav.empty(); + MonitoringConsole.Model.listPages().forEach(function(page) { + let tabId = page.id + '-tab'; + let css = "page-tab" + (page.active ? ' page-selected' : ''); + let pageTab = $('', {id: tabId, "class": css, text: page.name}); + if (page.active) { + pageTab.click(function() { + MonitoringConsole.Model.Settings.toggle(); + updatePageAndSelectionSettings(); + }); + } else { + pageTab.click(() => onPageChange(MonitoringConsole.Model.Page.changeTo(page.id))); + } + nav.append(pageTab); + }); + let addPage = $('', {id: 'addPageTab', 'class': 'page-tab'}).html('+'); + addPage.click(() => onPageChange(MonitoringConsole.Model.Page.create('(Unnamed)'))); + nav.append(addPage); } - function renderPageTabs() { - var bar = $("#pagesTabs"); - bar.empty(); - MonitoringConsole.listPages().forEach(function(page) { - var tabId = page.id + '-tab'; - var css = "page-tab" + (page.active ? ' page-selected' : ''); - //TODO when clicking active page tab the options could open/close - var newTab = $('', {id: tabId, "class": css, text: page.name}); - newTab.click(function() { - onPageChange(MonitoringConsole.Page.changeTo(page.id)); - }); - bar.append(newTab); - }); - var addPage = $('', {id: 'addPageTab', 'class': 'page-tab'}).html('+'); - addPage.click(function() { - onPageChange(MonitoringConsole.Page.create('(Unnamed)')); - }); - bar.append(addPage); + /** + * Updates the DOM with the page and selection settings so it reflects current model state + */ + function updatePageAndSelectionSettings() { + let panelConsole = $('#console'); + if (MonitoringConsole.Model.Settings.isDispayed()) { + if (!panelConsole.hasClass('state-show-settings')) { + panelConsole.addClass('state-show-settings'); + } + let panelSettings = $('#panel-settings'); + panelSettings + .empty() + .append($('