mirror of
https://github.com/EDCD/EDDN.git
synced 2025-04-27 05:32:13 +03:00
commit 84c75c17f76cff7e576402dd2a77eb52717e3758 Author: Athanasius <gitlab@miggy.org> Date: Wed Jun 24 20:00:58 2020 +0100 Software jsGrid now uses cached data when sorting/drill changed commit a9ed1d902c8301d62348eaa684eda5010a846ca1 Author: Athanasius <gitlab@miggy.org> Date: Wed Jun 24 16:49:47 2020 +0100 Convert Schemas to jsGrid commit 26fe4a7e0c997d71411d5159adafad24cff5dc74 Author: Athanasius <gitlab@miggy.org> Date: Wed Jun 24 16:20:57 2020 +0100 Change schemas HTML to just div.tables commit 45c86a901f8627a23f9b5c573b15c615cff40d8f Author: Athanasius <gitlab@miggy.org> Date: Wed Jun 24 16:19:34 2020 +0100 Remove defunct Uploaders section from index.html commit 3680197f86c805a1472f2538f3135c8698201f4d Author: Athanasius <gitlab@miggy.org> Date: Wed Jun 24 15:43:34 2020 +0100 Style today/yesterday "0" as per boostrap td.warning The elements/classes/IDs are different so can't just have it use the actual bootstrap CSS. commit ef855586305033854a3e74663f607c3ff68d3534 Author: Athanasius <gitlab@miggy.org> Date: Wed Jun 24 15:17:20 2020 +0100 Sanitises producing the Softwares data array We were first making a dictionary only to immediately use it to make an array for jsgrid/highcharts to use, then never using the dictionary again. So, just generate the array to start with, no intermediate step. This also sanitised some of the variable names so it's more obvious what data they hold. commit d5be067bb3bbe8837909e437f6ed099c65f66be7 Author: Athanasius <gitlab@miggy.org> Date: Tue Jun 23 20:10:20 2020 +0100 Set padding on td elements to get size correct No need for that 37px line-height, it was a consequence of this padding on non-jsgrid table cells. commit 109540131361cb6b44f55c506d913da6d43f3af2 Author: Athanasius <gitlab@miggy.org> Date: Tue Jun 23 19:58:32 2020 +0100 Clicking back out of Software drilldown working * Also fixes CSS so all jsgrid rows are that 37px height. commit f4c27fb53b33c3c28b7ad60b8e908a5dc330cce5 Author: Athanasius <gitlab@miggy.org> Date: Tue Jun 23 19:39:02 2020 +0100 'side square' colouring on sorts and drilldown commit 6c79f4b92bf1f9c73f760588e78ff874db2254a6 Author: Athanasius <gitlab@miggy.org> Date: Tue Jun 23 18:43:53 2020 +0100 Drilldown *in* for Software -> Versions now works commit 353b379d5a49aab826908f2c95f758d5fdd0114e Author: Athanasius <gitlab@miggy.org> Date: Tue Jun 23 17:56:54 2020 +0100 Starts work on displaying software -> version table drilldowns if/else in place, and the else handling the software*names*/totals table still works. commit fe2f6f0e73787512c8b5ca511871792a393894ce Author: Athanasius <gitlab@miggy.org> Date: Tue Jun 23 16:58:32 2020 +0100 Sort order on different column is now the same as on previous column If we start on Today/Ascending then: Yesterday -> Yesterday/Asc Total -> Total/Asc Total -> Total/Desc Today -> Today/Desc Today -> Today/Asc Yesterday -> Yesterday/Asc Total -> Total/Asc Total -> Total/Desc i.e. it takes a second click on a column to change to the opposite sort order, not always defaulting to 'Asc' when you change column. commit 9c471d2cebb5e26bca64f968b6846279fb488d84 Author: Athanasius <gitlab@miggy.org> Date: Tue Jun 23 16:37:26 2020 +0100 Now changing the pie chart depending on current sort column commit c1f07a38700860a859647cd563f0133965d9068d Author: Athanasius <gitlab@miggy.org> Date: Tue Jun 23 12:24:33 2020 +0100 Remember prior 'softwares' sort and re-apply for new data This defaults on first load as per: var softwaresSort = ... commit fa560cadd754fd5a934b9c926befe4d40651aa61 Author: Athanasius <gitlab@miggy.org> Date: Tue Jun 23 08:42:44 2020 +0100 Sane initial sorting, and some cleanup. * We can specify a sort column and order, no need for the 'reverse' hack, which is just going to confuse users and doesn't cause initial sorting anyway. * The softwaresTotal list can just be an Array, and then actually matches what we were doing with jsGridSoftwareByName[] anyway, so just use it. commit 05cc80d09b8107b94313481d20fdf3a94bd71b7a Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 21:53:59 2020 +0100 Documents the data, and why it's in different formats commit b567a1b452a5685fe296c9d8a3eb7aaae35d291f Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 21:43:30 2020 +0100 Remove the long-since defunct dataUploaders bits commit 282dd9df4d5200a3cc59e21005275440e1550464 Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 21:41:13 2020 +0100 Moving closer to understanding the data. * We'll now use an id'd div to put in a jsGrid set of divs per: 1) The totals per software 2) Each software's versions counts Currently this is working to display the software name totals, and attempts to add the per-software version-counts tables, but treats the data incorrectly. commit 5bcf4fb0a23315f2d3029f0f328a5c7a4a8bdf1a Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 21:40:24 2020 +0100 div for software tables needs an id, not a class. We want to put one div inside it per overal softwares and then one per specific software. commit 90e1a34d4cda8e960dcd13b1f7000af8904a614e Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 18:16:56 2020 +0100 Main Software table and pie chart rendering correctly * Having to force the row line-height to what FF ends up with in the old code, which is ~37px, not the ~20px specified. * Applying data-name to the .square elements so as to then later set their background colour. Can't do this as they're created as the data isn't yet in place to lookup the colour. * Removes the commented out old version of all that code, as it's now jsgrid-ised in that rowRenderer. commit dfd2d09513fb4dafe1bc4ec0e3a9fb32ba7518c1 Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 17:41:08 2020 +0100 Fix rendering of custom rowRenderer Software rows * As jsGrid adds its own table there's no need to have our own, just use the .table-responsive div * Use a rowRenderer function based on the existing code to make the row * But that means needing to ensure the <td> styling manually, as jsgrid does one table for its header and another for the data. It would apply the configured styling to botj, but with the custom rowRenderer that doesn't happen. commit 70bb2e822e4e5d8253f308b4c862127126731637 Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 16:55:20 2020 +0100 Enable the Software main pie chart to actually render * The code to add the data points to it initially had been commented out. * That 'sort the data' also is the only thing populating softwaresTotal. commit 61e1a17a1fba63cdcc7ce016b304c39670d90a11 Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 16:04:51 2020 +0100 CSS: Prevent jsGrid tables from showing un-necessary scrollbars commit 1109bf57122428b6603fcea9836f0b95c36ee31b Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 15:49:26 2020 +0100 Placeholder to implement clicking into drilldowns For now I want to just get the generation of drilldowns converted to jsGrid. Then I'll look into hooking up clicking the softwareName causing switching to displaying its drilldown. commit 87b78c98a0f59f10010c29abc3ee40f1ca3536e1 Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 14:54:30 2020 +0100 Numeric sorting now DESC first-click commit 571a058a3fa3658ef4b7c4f3f40beab37585eb33 Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 14:13:56 2020 +0100 More progress. Headings, comma-formatted numbers, widths... * Column added for the high-chart colours, not yet implemented fully. This will rely on implementing a custom jsGrid field. * comma-separated thousands formatting on the numbers. * Some width settings to match the pre-jsGrid layout. commit a73f069d014c9ff5ea98158afe4ea993f2109a2c Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 12:52:12 2020 +0100 Replace Software table with sortable jsgrid This is very rough and ready, but works: * Defaults to no sort * Sort will initially be ascending, we want descending * All the old styling is lost * Not yet handling click-through to per-softwareName version tables commit 2afb5f435617ed4fec9771a0f5e95ceef5fdd671 Author: Athanasius <gitlab@miggy.org> Date: Mon Jun 22 12:51:52 2020 +0100 Include jsgrid css and js
993 lines
41 KiB
JavaScript
993 lines
41 KiB
JavaScript
/* vim: wrapmargin=0 textwidth=0 tabstop=4 softtabstop=4 expandtab shiftwidth=4
|
||
*/
|
||
var updateInterval = 60000,
|
||
|
||
monitorEndPoint = 'https://eddn.edcd.io:9091/',
|
||
|
||
//gatewayBottlePort = 8080,
|
||
gatewayBottlePort = 4430,
|
||
relayBottlePort = 9090,
|
||
|
||
gateways = [
|
||
'eddn.edcd.io'
|
||
], //TODO: Must find a way to bind them to monitor
|
||
|
||
relays = [
|
||
'eddn.edcd.io'
|
||
]; //TODO: Must find a way to bind them to monitor
|
||
|
||
var stats = {
|
||
'gateways' : {},
|
||
'relays' : {}
|
||
}; // Stats placeholder
|
||
|
||
formatNumber = function(num) {
|
||
return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")
|
||
}
|
||
|
||
var makeSlug = function(str) {
|
||
var slugcontent_hyphens = str.replace(/\s/g,'-');
|
||
var finishedslug = slugcontent_hyphens.replace(/[^a-zA-Z0-9\-]/g,'');
|
||
return finishedslug.toLowerCase();
|
||
}
|
||
|
||
var makeName = function(str) {
|
||
var match = /^https:\/\/eddn.edcd.io\/schemas\/(\w)(\w*)\/(\d+)$/.exec(str);
|
||
if(match)
|
||
{
|
||
return match[1].toUpperCase() + match[2] + " v" + match[3];
|
||
}
|
||
|
||
var match = /^https:\/\/eddn.edcd.io\/schemas\/(\w)(\w*)\/(\d+)\/test$/.exec(str);
|
||
if(match)
|
||
{
|
||
return match[1].toUpperCase() + match[2] + " v" + match[3] + " [TEST]";
|
||
}
|
||
|
||
return str;
|
||
}
|
||
|
||
secondsToDurationString = function(seconds) {
|
||
var hours = Math.floor(seconds / 3600);
|
||
var minutes = Math.floor((seconds - (hours * 3600)) / 60);
|
||
var seconds = seconds - (hours * 3600) - (minutes * 60);
|
||
var days = 0;
|
||
|
||
if (hours > 24) {
|
||
days = Math.floor(hours / 24)
|
||
hours = Math.floor((hours - days * 24) / 3600);
|
||
}
|
||
|
||
if (hours < 10) {hours = "0" + hours;}
|
||
if (minutes < 10) {minutes = "0" + minutes;}
|
||
if (seconds < 10) {seconds = "0" + seconds;}
|
||
|
||
if (days > 0) {
|
||
return days + "d " + hours + ":" + minutes + ":" + seconds;
|
||
}
|
||
else {
|
||
return hours + ":" + minutes + ":" + seconds;
|
||
}
|
||
}
|
||
|
||
|
||
var drillDownSoftware = false;
|
||
var currentDrillDown = false;
|
||
|
||
var softwaresSort = { field: 'today', order: 'desc' }; // Very first load sort order
|
||
var softwaresData = new Array(); // The last data we got from API
|
||
var softwaresViewData = new Array(); // The data for the current view
|
||
var softwaresVersion = {};
|
||
|
||
var softwaresJsGridDataController = function () {
|
||
console.log('softwares -> jsGrid.controller.loadData() returning %o', softwaresViewData);
|
||
return softwaresViewData;
|
||
};
|
||
|
||
/*
|
||
* Create a new jsGrid and HighChart for Softwares
|
||
*/
|
||
var softwaresNewJsGrid = function () {
|
||
var chart = $('#software .chart').highcharts(),
|
||
series = chart.get('softwares');
|
||
var newJsGrid;
|
||
if (currentDrillDown) {
|
||
newJsGrid = $("#table-softwares").jsGrid({
|
||
width: "100%",
|
||
|
||
filtering: false,
|
||
inserting: false,
|
||
editing: false,
|
||
sorting: true,
|
||
|
||
controller: {
|
||
loadData: softwaresJsGridDataController,
|
||
},
|
||
|
||
fields: [
|
||
{
|
||
title: "",
|
||
width: "30px",
|
||
sorting: false,
|
||
readOnly: true,
|
||
},
|
||
{
|
||
title: currentDrillDown,
|
||
width: "50%",
|
||
name: "name",
|
||
type: "text",
|
||
align: "left",
|
||
readOnly: true,
|
||
},
|
||
{
|
||
title: "Today hits",
|
||
name: "today",
|
||
type: "number",
|
||
align: "right",
|
||
readOnly: true,
|
||
css: "stat today",
|
||
itemTemplate: formatNumberJsGrid,
|
||
},
|
||
{
|
||
title: "Yesterday hits",
|
||
name: "yesterday",
|
||
type: "number",
|
||
align: "right",
|
||
readOnly: true,
|
||
css: "stat yesterday",
|
||
itemTemplate: formatNumberJsGrid,
|
||
},
|
||
{
|
||
title: "Total hits",
|
||
name: "total",
|
||
type: "number",
|
||
align: "right",
|
||
readOnly: true,
|
||
css: "stat total",
|
||
itemTemplate: formatNumberJsGrid,
|
||
},
|
||
],
|
||
|
||
rowRenderer: function(item) {
|
||
softwareSplit = item.name.split(' | ');
|
||
return $('<tr>').attr('data-type', 'parent').attr('data-name', item.name).on('mouseover', function(){
|
||
chart.get('software-' + makeSlug(item.name)).setState('hover');
|
||
chart.tooltip.refresh(chart.get('software-' + makeSlug(item.name)));
|
||
}).on('mouseout', function(){
|
||
if(chart.get('software-' + makeSlug(item.name)))
|
||
chart.get('software-' + makeSlug(item.name)).setState('');
|
||
chart.tooltip.hide();
|
||
}).append(
|
||
$('<td>').addClass('square').attr('data-name', item.name).css('width', '30px').css('padding', '8px')
|
||
).append(
|
||
$('<td>').html('<strong>' + item.name + '</strong>').css('cursor','pointer').css('width', '50%')
|
||
)
|
||
.append(
|
||
$('<td>').addClass(item.today ? 'stat today' : 'warning').html(formatNumber(item.today || 0))
|
||
)
|
||
.append(
|
||
$('<td>').addClass(item.yesterday ? 'stat yesterday' : 'warning').html(formatNumber(item.yesterday || 0))
|
||
)
|
||
.append(
|
||
$('<td>').addClass('stat total').html('<strong>' + formatNumber(item.total) + '</strong>')
|
||
);
|
||
},
|
||
|
||
onRefreshed: function(grid) {
|
||
// Gets fired when sort is changed
|
||
//console.log('softwares.onRefreshed(): %o', grid);
|
||
if (grid && grid.grid && grid.grid._sortField) {
|
||
//console.log(' grid sort is: %o, %o', grid.grid._sortField.name, grid.grid._sortOrder);
|
||
//console.log(' saved sort is: %o', softwaresSort);
|
||
if (softwaresSort.field != grid.grid._sortField.name) {
|
||
softwaresSort.field = grid.grid._sortField.name;
|
||
$("#table-softwares").jsGrid("sort", softwaresSort);
|
||
return;
|
||
} else {
|
||
softwaresSort.order = grid.grid._sortOrder;
|
||
}
|
||
$.each(softwaresViewData, function(key, values) {
|
||
|
||
if(!chart.get('software-' + makeSlug(values.name)))
|
||
{
|
||
//console.log('Adding data point sort is: %o', softwaresSort.field);
|
||
// Populates the data into the overall Software pie chart as per current sort column
|
||
series.addPoint({id: 'software-' + makeSlug(values.name), name: values.name, y: parseInt(values[grid.grid._sortField.name]), drilldown: true}, false);
|
||
} else {
|
||
// Populates the data into the overall Software pie chart as per current sort column
|
||
chart.get('software-' + makeSlug(values.name)).update(parseInt(values[grid.grid._sortField.name]), false);
|
||
}
|
||
$(".square[data-name='" + this.name + "']").css('background', chart.get('software-' + makeSlug(values.name)).color);
|
||
});
|
||
}
|
||
chart.redraw();
|
||
},
|
||
});
|
||
|
||
$("#table-softwares table .jsgrid-header-row th:eq(0)").html('<span class="glyphicon glyphicon-remove"></span>')
|
||
.css('cursor','pointer')
|
||
.on('click', function(event) {
|
||
//console.log('softwares: click! %o', event);
|
||
currentDrillDown = false;
|
||
/*
|
||
* No longer drilling down, so need to reset the data
|
||
*/
|
||
softwaresViewData = new Array();
|
||
softwaresData.forEach(function(software, s) {
|
||
softwareSplit = software.name.split(' | ');
|
||
name = softwareSplit[0];
|
||
var sw = softwaresViewData.find(o => o.name === name);
|
||
if(!sw) {
|
||
softwaresViewData.push({ 'name': name, 'today': software.today, 'yesterday': software.yesterday, 'total': software.total});
|
||
sw = softwaresViewData.find(o => o.name === name);
|
||
} else {
|
||
sw['today'] += software.today;
|
||
sw['yesterday'] += software.yesterday;
|
||
sw['total'] += software.total;
|
||
}
|
||
});
|
||
softwaresNewJsGrid();
|
||
});
|
||
|
||
} else {
|
||
// Not drilling down
|
||
newJsGrid = $("#table-softwares").jsGrid({
|
||
width: "100%",
|
||
|
||
filtering: false,
|
||
inserting: false,
|
||
editing: false,
|
||
sorting: true,
|
||
autoload: false,
|
||
|
||
controller: {
|
||
loadData: softwaresJsGridDataController
|
||
},
|
||
|
||
fields: [
|
||
{
|
||
title: "",
|
||
width: "30px",
|
||
sorting: false,
|
||
readOnly: true,
|
||
},
|
||
{
|
||
title: "Software name",
|
||
width: "50%",
|
||
name: "name",
|
||
type: "text",
|
||
align: "left",
|
||
readOnly: true,
|
||
},
|
||
{
|
||
title: "Today hits",
|
||
name: "today",
|
||
type: "number",
|
||
align: "right",
|
||
readOnly: true,
|
||
css: "stat today",
|
||
itemTemplate: formatNumberJsGrid,
|
||
},
|
||
{
|
||
title: "Yesterday hits",
|
||
name: "yesterday",
|
||
type: "number",
|
||
align: "right",
|
||
readOnly: true,
|
||
css: "stat yesterday",
|
||
itemTemplate: formatNumberJsGrid,
|
||
},
|
||
{
|
||
title: "Total hits",
|
||
name: "total",
|
||
type: "number",
|
||
align: "right",
|
||
readOnly: true,
|
||
css: "stat total",
|
||
itemTemplate: formatNumberJsGrid,
|
||
},
|
||
],
|
||
|
||
rowRenderer: function(item) {
|
||
return $('<tr>').attr('data-type', 'parent').attr('data-name', item.name).on('click', function(event){
|
||
//console.log('softwares: click! %o', event);
|
||
currentDrillDown = item.name;
|
||
|
||
/*
|
||
* The data we need for this drilldown
|
||
*/
|
||
softwaresViewData = new Array();
|
||
softwaresData.forEach(function(software, s) {
|
||
softwareSplit = software.name.split(' | ');
|
||
var name = "";
|
||
if (currentDrillDown == softwareSplit[0]) {
|
||
name = softwareSplit[1];
|
||
} else {
|
||
return true;
|
||
}
|
||
var sw = softwaresViewData.find(o => o.name === name);
|
||
if(!sw) {
|
||
softwaresViewData.push({ 'name': name, 'today': software.today, 'yesterday': software.yesterday, 'total': software.total});
|
||
sw = softwaresViewData.find(o => o.name === name);
|
||
} else {
|
||
sw['today'] += software.today;
|
||
sw['yesterday'] += software.yesterday;
|
||
sw['total'] += software.total;
|
||
}
|
||
});
|
||
softwaresNewJsGrid();
|
||
}).on('mouseover', function(){
|
||
chart.get('software-' + makeSlug(item.name)).setState('hover');
|
||
chart.tooltip.refresh(chart.get('software-' + makeSlug(item.name)));
|
||
}).on('mouseout', function(){
|
||
if(chart.get('software-' + makeSlug(item.name)))
|
||
chart.get('software-' + makeSlug(item.name)).setState('');
|
||
chart.tooltip.hide();
|
||
}).append(
|
||
$('<td>').addClass('square').attr('data-name', item.name).css('width', '30px').css('padding', '8px')
|
||
).append(
|
||
$('<td>').html('<strong>' + item.name + '</strong>').css('cursor','pointer').css('width', '50%')
|
||
)
|
||
.append(
|
||
$('<td>').addClass(item.today ? 'stat today' : 'warning').html(formatNumber(item.today || 0))
|
||
)
|
||
.append(
|
||
$('<td>').addClass(item.yesterday ? 'stat yesterday' : 'warning').html(formatNumber(item.yesterday || 0))
|
||
)
|
||
.append(
|
||
$('<td>').addClass('stat total').html('<strong>' + formatNumber(item.total) + '</strong>')
|
||
);
|
||
},
|
||
|
||
onRefreshed: function(grid) {
|
||
// Gets fired when sort is changed
|
||
//console.log('softwares.onRefreshed(): %o', grid);
|
||
if (grid && grid.grid && grid.grid._sortField) {
|
||
//console.log(' grid sort is: %o, %o', grid.grid._sortField.name, grid.grid._sortOrder);
|
||
//console.log(' saved sort is: %o', softwaresSort);
|
||
if (softwaresSort.field != grid.grid._sortField.name) {
|
||
softwaresSort.field = grid.grid._sortField.name;
|
||
$("#table-softwares").jsGrid("sort", softwaresSort);
|
||
return;
|
||
} else {
|
||
softwaresSort.order = grid.grid._sortOrder;
|
||
}
|
||
$.each(softwaresViewData, function(key, values) {
|
||
|
||
if(!chart.get('software-' + makeSlug(values.name)))
|
||
{
|
||
//console.log('Adding data point sort is: %o', softwaresSort.field);
|
||
// Populates the data into the overall Software pie chart as per current sort column
|
||
series.addPoint({id: 'software-' + makeSlug(values.name), name: values.name, y: parseInt(values[grid.grid._sortField.name]), drilldown: true}, false);
|
||
} else {
|
||
// Populates the data into the overall Software pie chart as per current sort column
|
||
chart.get('software-' + makeSlug(values.name)).update(parseInt(values[grid.grid._sortField.name]), false);
|
||
}
|
||
$(".square[data-name='" + this.name + "']").css('background', chart.get('software-' + makeSlug(values.name)).color);
|
||
});
|
||
}
|
||
chart.redraw();
|
||
},
|
||
});
|
||
}
|
||
|
||
// Because we're using a controller for data we need to trigger it
|
||
$("#table-softwares").jsGrid("loadData");
|
||
// Re-apply the last stored sor
|
||
$("#table-softwares").jsGrid("sort", softwaresSort);
|
||
|
||
// Populate the chart with the data
|
||
series.remove(false);
|
||
series = chart.addSeries({
|
||
id: 'softwares',
|
||
name: 'Softwares',
|
||
type: 'pie',
|
||
data: []
|
||
});
|
||
$.each(softwaresViewData, function(key, values) {
|
||
field = $("#table-softwares").jsGrid("getSorting").field;
|
||
if(!chart.get('software-' + makeSlug(values.name)))
|
||
{
|
||
//console.log('Adding data point sort is: %o', softwaresSort.field);
|
||
// Populates the data into the overall Software pie chart as per current sort column
|
||
series.addPoint({id: 'software-' + makeSlug(values.name), name: values.name, y: parseInt(values[field]), drilldown: true}, false);
|
||
} else {
|
||
// Populates the data into the overall Software pie chart as per current sort column
|
||
chart.get('software-' + makeSlug(values.name)).update(parseInt(values[field]), false);
|
||
}
|
||
$(".square[data-name='" + this.name + "']").css('background', chart.get('software-' + makeSlug(values.name)).color);
|
||
});
|
||
|
||
chart.redraw();
|
||
|
||
$('#software').find(".stat").removeClass("warning").each(function() {
|
||
if ($(this).html() == "0")
|
||
$(this).addClass("warning");
|
||
});
|
||
|
||
return newJsGrid;
|
||
}
|
||
|
||
var doUpdateSoftwares = function()
|
||
{
|
||
var dToday = new Date(),
|
||
dYesterday = new (function(d){ d.setDate(d.getDate()-1); return d})(new Date),
|
||
|
||
yesterday = dYesterday.getUTCFullYear() + '-' + ("0" + (dYesterday.getUTCMonth() + 1)).slice(-2) + '-' + ("0" + (dYesterday.getUTCDate())).slice(-2),
|
||
today = dToday.getUTCFullYear() + '-' + ("0" + (dToday.getUTCMonth() + 1)).slice(-2) + '-' + ("0" + (dToday.getUTCDate())).slice(-2);
|
||
|
||
/*
|
||
* Gathering the data per a "<softwareName> | <softwareVersion>" tuple takes two calls.
|
||
*
|
||
* 1) First a /getSoftwares/?dateStart=<yesterday>&dateEnd=<today>
|
||
*
|
||
* This returns an object with two top level keys, one for each date. The value
|
||
* for each is another object with "<softwareName> | <softwareVersion>" as each key,
|
||
* and the value as the count for that tuple.
|
||
*
|
||
* 2) Then the lifetime totals for each "<softwareName> | <softwareVersion>" tuple, from
|
||
* /getTotalSoftwares/
|
||
*
|
||
* This returns an object with "<softwareName> | <softwareVersion>" tuples as keys,
|
||
* the values being the lifetime totals for each tuple.
|
||
*
|
||
* The calls are nested here, so only the inner .ajax() has access to the totality of data.
|
||
*/
|
||
$.ajax({
|
||
dataType: "json",
|
||
url: monitorEndPoint + 'getSoftwares/?dateStart=' + yesterday + '&dateEnd = ' + today,
|
||
success: function(softwaresTodayYesterday){
|
||
$.ajax({
|
||
dataType: "json",
|
||
url: monitorEndPoint + 'getTotalSoftwares/',
|
||
success: function(softwaresTotals){
|
||
// Might happen when nothing is received...
|
||
if(softwaresTodayYesterday[yesterday] == undefined)
|
||
softwaresTodayYesterday[yesterday] = [];
|
||
if(softwaresTodayYesterday[today] == undefined)
|
||
softwaresTodayYesterday[today] = [];
|
||
|
||
/*
|
||
* Prepare 'softwaresData' dictionary:
|
||
*
|
||
* key: software name, including the version
|
||
* value: dictionary with counts for: today, yesterday, total (all time)
|
||
*/
|
||
softwaresData = new Array();
|
||
$.each(softwaresTotals, function(softwareName, total){
|
||
var sw = { 'name': softwareName, 'today': 0, 'yesterday': 0, 'total': parseInt(total)};
|
||
|
||
sw['today'] += parseInt(softwaresTodayYesterday[today][softwareName] || 0);
|
||
sw['yesterday'] += parseInt(softwaresTodayYesterday[yesterday][softwareName] || 0);
|
||
|
||
softwaresData.push(sw);
|
||
});
|
||
|
||
/*
|
||
* Now the data we need for the current view (overall data or a drilldown of a software)
|
||
*/
|
||
softwaresViewData = new Array();
|
||
softwaresData.forEach(function(software, s) {
|
||
softwareSplit = software.name.split(' | ');
|
||
var name = "";
|
||
if (currentDrillDown) {
|
||
if (currentDrillDown == softwareSplit[0]) {
|
||
name = softwareSplit[1];
|
||
} else {
|
||
return true;
|
||
}
|
||
} else {
|
||
name = softwareSplit[0];
|
||
}
|
||
var sw = softwaresViewData.find(o => o.name === name);
|
||
if(!sw) {
|
||
softwaresViewData.push({ 'name': name, 'today': software.today, 'yesterday': software.yesterday, 'total': software.total});
|
||
sw = softwaresViewData.find(o => o.name === name);
|
||
} else {
|
||
sw['today'] += software.today;
|
||
sw['yesterday'] += software.yesterday;
|
||
sw['total'] += software.total;
|
||
}
|
||
});
|
||
|
||
// Ensure we have the jsGrid added
|
||
if (! $("#table-softwares").length ) {
|
||
// Append a new DIV for this jsGrid to the "#software #tables" div
|
||
$('#software #tables').append(
|
||
$('<div/>').addClass('jsGridTable').attr('id', 'table-softwares')
|
||
);
|
||
} else {
|
||
// Store the last selected sort so we can apply it to the new version
|
||
softwaresSort = $("#table-softwares").jsGrid("getSorting");
|
||
}
|
||
|
||
newJsGrid = softwaresNewJsGrid();
|
||
|
||
$('#software').find(".update_timestamp").html(d.toString("yyyy-MM-dd HH:mm:ss"));
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
var schemasSort = { field: 'today', order: 'desc' }; // Very first load sort order
|
||
var schemasData = new Array();
|
||
|
||
var doUpdateSchemas = function()
|
||
{
|
||
var dToday = new Date(),
|
||
dYesterday = new (function(d){ d.setDate(d.getDate()-1); return d})(new Date),
|
||
|
||
yesterday = dYesterday.getUTCFullYear() + '-' + ("0" + (dYesterday.getUTCMonth() + 1)).slice(-2) + '-' + ("0" + (dYesterday.getUTCDate())).slice(-2),
|
||
today = dToday.getUTCFullYear() + '-' + ("0" + (dToday.getUTCMonth() + 1)).slice(-2) + '-' + ("0" + (dToday.getUTCDate())).slice(-2);
|
||
|
||
$.ajax({
|
||
dataType: "json",
|
||
url: monitorEndPoint + 'getSchemas/?dateStart=' + yesterday + '&dateEnd = ' + today,
|
||
success: function(schemasTodayYesterday){
|
||
// Might happen when nothing is received...
|
||
if(schemasTodayYesterday[yesterday] == undefined)
|
||
schemaTodayYesterday[yesterday] = [];
|
||
if(schemasTodayYesterday[today] == undefined)
|
||
schemasTodayYesterday[today] = [];
|
||
|
||
$.ajax({
|
||
dataType: "json",
|
||
url: monitorEndPoint + 'getTotalSchemas/',
|
||
success: function(schemasTotals){
|
||
var chart = $('#schemas .chart').highcharts(),
|
||
series = chart.get('schemas');
|
||
|
||
/*
|
||
* Prepare 'schemasData' dictionary
|
||
*/
|
||
schemasData = new Array();
|
||
$.each(schemasTotals, function(schema, total) {
|
||
schemaName = schema.replace('http://schemas.elite-markets.net/eddn/', 'https://eddn.edcd.io/schemas/');
|
||
// Due to the schema renames and us merging them there could be more than one
|
||
// row of data input per schema
|
||
var sch = schemasData.find(o => o.name === schemaName);
|
||
if (!sch) {
|
||
schemasData.push({ 'name': schemaName, 'today': 0, 'yesterday': 0, 'total': parseInt(total)});
|
||
sch = schemasData.find(o => o.name === schemaName);
|
||
} else {
|
||
sch['total'] += parseInt(total);
|
||
}
|
||
});
|
||
|
||
// Today
|
||
$.each(schemasTodayYesterday[today], function(schema, hits) {
|
||
schemaName = schema.replace('http://schemas.elite-markets.net/eddn/', 'https://eddn.edcd.io/schemas/');
|
||
var sch = schemasData.find(o => o.name === schemaName);
|
||
sch['today'] += parseInt(hits);
|
||
});
|
||
// Yesterday
|
||
$.each(schemasTodayYesterday[yesterday], function(schema, hits) {
|
||
schemaName = schema.replace('http://schemas.elite-markets.net/eddn/', 'https://eddn.edcd.io/schemas/');
|
||
var sch = schemasData.find(o => o.name === schemaName);
|
||
sch['yesterday'] += parseInt(hits);
|
||
});
|
||
|
||
// Ensure we have the jsGrid added
|
||
if (! $("#table-schemas").length ) {
|
||
// Append a new DIV for this jsGrid to the "#schemas #tables" div
|
||
$('#schemas #tables').append(
|
||
$('<div/>').addClass('jsGridTable').attr('id', 'table-schemas')
|
||
);
|
||
} else {
|
||
// Store the last selected sort so we can apply it to the new version
|
||
schemasSort = $("#table-schemas").jsGrid("getSorting");
|
||
}
|
||
|
||
newJsGrid = $("#table-schemas").jsGrid({
|
||
width: "100%",
|
||
|
||
filtering: false,
|
||
inserting: false,
|
||
editing: false,
|
||
sorting: true,
|
||
autoload: false,
|
||
|
||
data: schemasData,
|
||
|
||
fields: [
|
||
{
|
||
title: "",
|
||
width: "30px",
|
||
name: "chartslug",
|
||
sorting: false,
|
||
readOnly: true,
|
||
},
|
||
{
|
||
title: "Schema",
|
||
width: "50%",
|
||
name: "name",
|
||
type: "text",
|
||
align: "left",
|
||
readOnly: true,
|
||
},
|
||
{
|
||
title: "Today hits",
|
||
name: "today",
|
||
type: "number",
|
||
align: "right",
|
||
readOnly: true,
|
||
css: "stat today",
|
||
itemTemplate: formatNumberJsGrid,
|
||
},
|
||
{
|
||
title: "Yesterday hits",
|
||
name: "yesterday",
|
||
type: "number",
|
||
align: "right",
|
||
readOnly: true,
|
||
css: "stat yesterday",
|
||
itemTemplate: formatNumberJsGrid,
|
||
},
|
||
{
|
||
title: "Total hits",
|
||
name: "total",
|
||
type: "number",
|
||
align: "right",
|
||
readOnly: true,
|
||
css: "stat total",
|
||
itemTemplate: formatNumberJsGrid,
|
||
},
|
||
],
|
||
|
||
rowRenderer: function(item) {
|
||
return $('<tr>').attr('data-type', 'parent').attr('data-name', item.name).on('mouseover', function(){
|
||
chart.get('schema-' + makeSlug(item.name)).setState('hover');
|
||
chart.tooltip.refresh(chart.get('schema-' + makeSlug(item.name)));
|
||
}).on('mouseout', function(){
|
||
if(chart.get('schema-' + makeSlug(item.name)))
|
||
chart.get('schema-' + makeSlug(item.name)).setState('');
|
||
chart.tooltip.hide();
|
||
}).append(
|
||
$('<td>').addClass('square').attr('data-name', item.name).css('width', '30px').css('padding', '8px')
|
||
).append(
|
||
$('<td>').html('<strong>' + makeName(item.name) + '</strong>').css('cursor','pointer').css('width', '50%')
|
||
)
|
||
.append(
|
||
$('<td>').addClass(item.today ? 'stat today' : 'warning').html(formatNumber(item.today || 0))
|
||
)
|
||
.append(
|
||
$('<td>').addClass(item.yesterday ? 'stat yesterday' : 'warning').html(formatNumber(item.yesterday || 0))
|
||
)
|
||
.append(
|
||
$('<td>').addClass('stat total').html('<strong>' + formatNumber(item.total) + '</strong>')
|
||
);
|
||
},
|
||
|
||
onRefreshed: function(grid) {
|
||
// Gets fired when sort is changed
|
||
//console.log('softwares.onRefreshed(): %o', grid);
|
||
if (grid && grid.grid && grid.grid._sortField) {
|
||
//console.log(' grid sort is: %o, %o', grid.grid._sortField.name, grid.grid._sortOrder);
|
||
//console.log(' saved sort is: %o', schemasSort);
|
||
if (schemasSort.field != grid.grid._sortField.name) {
|
||
schemasSort.field = grid.grid._sortField.name;
|
||
$("#table-schemas").jsGrid("sort", schemasSort);
|
||
return;
|
||
} else {
|
||
schemasSort.order = grid.grid._sortOrder;
|
||
}
|
||
$.each(schemasData, function(key, values) {
|
||
if(!chart.get('schema-' + makeSlug(values.name)))
|
||
{
|
||
//console.log('Adding data point sort is: %o', schemasSort.field);
|
||
// Populates the data into the overall Software pie chart as per current sort column
|
||
series.addPoint({id: 'schema-' + makeSlug(values.name), name: values.name, y: parseInt(values[grid.grid._sortField.name]), drilldown: true}, false);
|
||
} else {
|
||
// Populates the data into the overall Software pie chart as per current sort column
|
||
chart.get('schema-' + makeSlug(values.name)).update(parseInt(values[grid.grid._sortField.name]), false);
|
||
}
|
||
$(".square[data-name='" + this.name + "']").css('background', chart.get('schema-' + makeSlug(values.name)).color);
|
||
});
|
||
}
|
||
chart.redraw();
|
||
},
|
||
});
|
||
|
||
// Re-apply the last stored sort
|
||
$("#table-schemas").jsGrid("sort", schemasSort);
|
||
|
||
chart.redraw();
|
||
|
||
$('#schemas').find(".stat").removeClass("warning").each(function() {
|
||
if ($(this).html() == "0")
|
||
$(this).addClass("warning");
|
||
});
|
||
|
||
$('#schemas').find(".update_timestamp").html(d.toString("yyyy-MM-dd HH:mm:ss"));
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
var doUpdates = function(type){
|
||
$("select[name=" + type + "] option").each(function(){
|
||
var currentItem = $(this).html(),
|
||
isSelected = $(this).is(':selected');
|
||
|
||
$.ajax({
|
||
dataType: "json",
|
||
url: $(this).val(),
|
||
success: function(data){
|
||
d = new Date();
|
||
|
||
stats[type][currentItem]['lastUpdate'] = d.toString("yyyy-MM-dd HH:mm:ss");
|
||
stats[type][currentItem]['last'] = data;
|
||
|
||
if(isSelected)
|
||
showStats(type, currentItem);
|
||
|
||
var chart = $("#" + type + " .chart[data-name='" + currentItem + "']").highcharts();
|
||
|
||
shift = chart.get('inbound').data.length > 60;
|
||
chart.get('inbound').addPoint([d.getTime(), (data['inbound'] || {})['1min'] || 0], true, shift);
|
||
|
||
if(type == 'gateways')
|
||
{
|
||
shift = chart.get('invalid').data.length > 60;
|
||
chart.get('invalid').addPoint([d.getTime(), (data['invalid'] || {})['1min'] || 0], true, shift);
|
||
}
|
||
|
||
if(type == 'relays')
|
||
{
|
||
shift = chart.get('duplicate').data.length > 60;
|
||
chart.get('duplicate').addPoint([d.getTime(), (data['duplicate'] || {})['1min'] || 0], true, shift);
|
||
}
|
||
|
||
shift = chart.get('outbound').data.length > 60;
|
||
chart.get('outbound').addPoint([d.getTime(), (data['outbound'] || {})['1min'] || 0], true, shift);
|
||
}
|
||
});
|
||
});
|
||
};
|
||
|
||
var showStats = function(type, currentItem){
|
||
var el = $('#' + type),
|
||
currentItemStats = stats[type][currentItem]['last'];
|
||
|
||
el.find(".inbound_1min").html((currentItemStats['inbound'] || {})['1min'] || 0);
|
||
el.find(".inbound_5min").html((currentItemStats["inbound"] || {})['5min'] || 0);
|
||
el.find(".inbound_60min").html((currentItemStats["inbound"] || {})['60min'] || 0);
|
||
|
||
if(type == 'gateways')
|
||
{
|
||
el.find(".invalid_1min").html((currentItemStats["invalid"] || {})['1min'] || 0);
|
||
el.find(".invalid_5min").html((currentItemStats["invalid"] || {})['5min'] || 0);
|
||
el.find(".invalid_60min").html((currentItemStats["invalid"] || {})['60min'] || 0);
|
||
|
||
el.find(".outdated_1min").html((currentItemStats["outdated"] || {})['1min'] || 0);
|
||
el.find(".outdated_5min").html((currentItemStats["outdated"] || {})['5min'] || 0);
|
||
el.find(".outdated_60min").html((currentItemStats["outdated"] || {})['60min'] || 0);
|
||
}
|
||
|
||
if(type == 'relays')
|
||
{
|
||
el.find(".duplicate_1min").html((currentItemStats["duplicate"] || {})['1min'] || 0);
|
||
el.find(".duplicate_5min").html((currentItemStats["duplicate"] || {})['5min'] || 0);
|
||
el.find(".duplicate_60min").html((currentItemStats["duplicate"] || {})['60min'] || 0);
|
||
}
|
||
|
||
el.find(".outbound_1min").html((currentItemStats["outbound"] || {})['1min'] || 0);
|
||
el.find(".outbound_5min").html((currentItemStats["outbound"] || {})['5min'] || 0);
|
||
el.find(".outbound_60min").html((currentItemStats["outbound"] || {})['60min'] || 0);
|
||
|
||
el.find(".update_timestamp").html(stats[type][currentItem]['lastUpdate']);
|
||
el.find(".version").html(currentItemStats['version'] || 'N/A');
|
||
|
||
if (currentItemStats['uptime'])
|
||
el.find(".uptime").html(secondsToDurationString(currentItemStats['uptime']));
|
||
else
|
||
el.find(".uptime").html('N/A');
|
||
|
||
el.find(".stat").removeClass("warning").each(function() {
|
||
if ($(this).html() == "0")
|
||
$(this).addClass("warning");
|
||
});
|
||
|
||
el.find(".chart").hide();
|
||
el.find(".chart[data-name='" + currentItem + "']").show();
|
||
$(window).trigger('resize'); // Fix wrong size in chart
|
||
};
|
||
|
||
|
||
|
||
/**
|
||
* Launch monitoring
|
||
*/
|
||
var start = function(){
|
||
Highcharts.setOptions({global: {useUTC: false}});
|
||
|
||
// Grab gateways
|
||
//gateways = gateways.sort();
|
||
$.each(gateways, function(k, gateway){
|
||
gateway = gateway.replace('tcp://', '');
|
||
gateway = gateway.replace(':8500', '');
|
||
|
||
$("select[name=gateways]").append($('<option>', {
|
||
value: 'https://' + gateway + ':' + gatewayBottlePort + '/stats/',
|
||
text : gateway
|
||
}));
|
||
|
||
$('#gateways .charts').append(
|
||
$('<div>').addClass('chart')
|
||
.css('width', '100%')
|
||
.attr('data-name', gateway)
|
||
);
|
||
|
||
$("#gateways .chart[data-name='" + gateway + "']").highcharts({
|
||
chart: {
|
||
type: 'spline', animation: Highcharts.svg
|
||
},
|
||
title: { text: '', style: {display: 'none'} },
|
||
xAxis: {
|
||
type: 'datetime',
|
||
tickPixelInterval: 150
|
||
},
|
||
yAxis: {
|
||
title: {text: ''},
|
||
plotLines: [{value: 0, width: 1, color: '#808080'}],
|
||
min: 0
|
||
},
|
||
tooltip: { enabled: false },
|
||
credits: { enabled: false },
|
||
exporting: { enabled: false },
|
||
series: [
|
||
{id: 'inbound', data: [], name: 'Messages received', zIndex: 300},
|
||
{id: 'invalid', data: [], name: 'Invalid messages', zIndex: 1},
|
||
{id: 'outdated', data: [], name: 'Outdated messages', zIndex: 1},
|
||
{id: 'outbound', data: [], name: 'Messages passed to relay', zIndex: 200}
|
||
]
|
||
}).hide();
|
||
|
||
stats['gateways'][gateway] = {};
|
||
});
|
||
|
||
doUpdates('gateways');
|
||
setInterval(function(){
|
||
doUpdates('gateways');
|
||
}, updateInterval);
|
||
|
||
// Grab relays
|
||
//relays = relays.sort();
|
||
$.each(relays, function(k, relay){
|
||
$("select[name=relays]").append($('<option>', {
|
||
value: 'https://' + relay + ':' + relayBottlePort + '/stats/',
|
||
text : relay
|
||
}));
|
||
|
||
$('#relays .charts').append(
|
||
$('<div>').addClass('chart')
|
||
.css('width', '100%')
|
||
.attr('data-name', relay)
|
||
);
|
||
|
||
$("#relays .chart[data-name='" + relay + "']").highcharts({
|
||
chart: {
|
||
type: 'spline', animation: Highcharts.svg,
|
||
events: {
|
||
load: function(){ setTimeout(function(){$(window).trigger('resize');}, 250); }
|
||
},
|
||
marginRight: 10
|
||
},
|
||
title: { text: '', style: {display: 'none'} },
|
||
xAxis: {
|
||
type: 'datetime',
|
||
tickPixelInterval: 150
|
||
},
|
||
yAxis: {
|
||
title: {text: ''},
|
||
plotLines: [{value: 0, width: 1, color: '#808080'}],
|
||
min: 0
|
||
},
|
||
tooltip: { enabled: false },
|
||
credits: { enabled: false },
|
||
exporting: { enabled: false },
|
||
series: [
|
||
{id: 'inbound', data: [], name: 'Messages received', zIndex: 300},
|
||
{id: 'duplicate', data: [], name: 'Messages duplicate', zIndex: 1},
|
||
{id: 'outbound', data: [], name: 'Messages passed to subscribers', zIndex: 200}
|
||
]
|
||
}).hide();
|
||
|
||
stats['relays'][relay] = {};
|
||
});
|
||
|
||
doUpdates('relays');
|
||
setInterval(function(){
|
||
doUpdates('relays');
|
||
}, updateInterval);
|
||
|
||
// Grab software from monitor
|
||
$('#software .chart').highcharts({
|
||
chart: {
|
||
type: 'pie', animation: Highcharts.svg
|
||
},
|
||
title: { text: '', style: {display: 'none'} },
|
||
credits: { enabled: false },
|
||
tooltip: { headerFormat: '', pointFormat: '{point.name}: <b>{point.percentage:.1f}%</b>' },
|
||
legend: { enabled: false },
|
||
plotOptions: {pie: {allowPointSelect: false,dataLabels: { enabled: false }}},
|
||
series: [{
|
||
id: 'softwares',
|
||
name: 'Softwares',
|
||
type: 'pie',
|
||
data: []
|
||
}]
|
||
});
|
||
|
||
doUpdateSoftwares();
|
||
setInterval(function(){
|
||
doUpdateSoftwares();
|
||
}, updateInterval);
|
||
|
||
// Grab uploader from monitor
|
||
$('#uploaders .chart').highcharts({
|
||
chart: {
|
||
type: 'pie', animation: Highcharts.svg
|
||
},
|
||
title: { text: '', style: {display: 'none'} },
|
||
credits: { enabled: false },
|
||
tooltip: { headerFormat: '', pointFormat: '{point.name}: <b>{point.percentage:.1f}%</b>' },
|
||
legend: { enabled: false },
|
||
plotOptions: {pie: {allowPointSelect: false,dataLabels: { enabled: false }}},
|
||
series: [{
|
||
id: 'uploaders',
|
||
type: 'pie',
|
||
data: []
|
||
}]
|
||
});
|
||
|
||
// Grab schema from monitor
|
||
$('#schemas .chart').highcharts({
|
||
chart: {
|
||
type: 'pie', animation: Highcharts.svg
|
||
},
|
||
title: { text: '', style: {display: 'none'} },
|
||
credits: { enabled: false },
|
||
tooltip: { headerFormat: '', pointFormat: '{point.name}: <b>{point.percentage:.1f}%</b>' },
|
||
legend: { enabled: false },
|
||
plotOptions: {pie: {allowPointSelect: false,dataLabels: { enabled: false }}},
|
||
series: [{
|
||
id: 'schemas',
|
||
type: 'pie',
|
||
data: []
|
||
}]
|
||
});
|
||
|
||
doUpdateSchemas();
|
||
setInterval(function(){
|
||
doUpdateSchemas();
|
||
}, updateInterval);
|
||
|
||
// Attach events
|
||
$("select[name=gateways]").change(function(){
|
||
showStats('gateways', $(this).find('option:selected').html());
|
||
});
|
||
$("select[name=relays]").change(function(){
|
||
showStats('relays', $(this).find('option:selected').html());
|
||
});
|
||
}
|
||
|
||
/*
|
||
* JS Grid related functions
|
||
*/
|
||
|
||
/*
|
||
* Nicely format a number for jsGrid
|
||
*/
|
||
formatNumberJsGrid = function(value, item) {
|
||
return value.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
|
||
}
|
||
|
||
$(document).ready(function(){
|
||
start();
|
||
});
|