/** * logging */ define([ 'jquery', 'app/init', 'app/util', 'app/counter', 'bootbox', 'app/lib/resize' ], ($, Init, Util, Counter, bootbox) => { 'use strict'; let logData = []; // cache object for all log entries let logDataTable = null; // "Datatables" Object // Morris charts data let maxGraphDataCount = 30; // max date entries for a graph let chartData = {}; // chart Data object for all Morris Log graphs let config = { taskDialogId: 'pf-task-dialog', // id for map "task manager" dialog timestampCounterClass: 'pf-timestamp-counter', // class for "timestamp" counter taskDialogStatusAreaClass: 'pf-task-dialog-status', // class for "status" dynamic area taskDialogLogTableAreaClass: 'pf-task-dialog-table', // class for "log table" dynamic area logGraphClass: 'pf-log-graph', // class for all log Morris graphs moduleHeadlineIconClass: 'pf-module-icon-button' // class for toolbar icons in the head }; /** * updated "sync status" dynamic dialog area */ let updateSyncStatus = () => { // check if task manager dialog is open let logDialog = $('#' + config.taskDialogId); if(logDialog.length){ // dialog is open let statusArea = logDialog.find('.' + config.taskDialogStatusAreaClass); statusArea.destroyTooltips(true); requirejs(['text!templates/modules/sync_status.html', 'mustache'], (templateSyncStatus, Mustache) => { let data = { timestampCounterClass: config.timestampCounterClass, syncStatus: Init.syncStatus, isWebSocket: () => { return (Util.getSyncType() === 'webSocket'); }, isAjax: () => { return (Util.getSyncType() === 'ajax'); } }; let syncStatusElement = $(Mustache.render(templateSyncStatus, data )); Counter.destroyTimestampCounter(statusArea, true); statusArea.html(syncStatusElement); let counterElements = syncStatusElement.find('.' + config.timestampCounterClass); Counter.initTimestampCounter(counterElements); statusArea.initTooltips({ placement: 'right' }); }); } }; /** * shows the logging dialog */ let showDialog = () => { requirejs(['text!templates/dialog/task_manager.html', 'mustache'], function(templateTaskManagerDialog, Mustache){ let data = { id: config.taskDialogId, dialogDynamicAreaClass: Util.config.dynamicAreaClass, taskDialogStatusAreaClass: config.taskDialogStatusAreaClass, taskDialogLogTableAreaClass: config.taskDialogLogTableAreaClass }; let contentTaskManager = $( Mustache.render(templateTaskManagerDialog, data) ); let rowElementGraphs = contentTaskManager.find('.row'); let taskDialogLogTableAreaElement = contentTaskManager.find('.' + config.taskDialogLogTableAreaClass); let logTable = $('', { class: ['compact', 'stripe', 'order-column', 'row-border'].join(' ') }); taskDialogLogTableAreaElement.append(logTable); // init log table logDataTable = logTable.DataTable({ dom: '<"flex-row flex-between"<"flex-col"l><"flex-col"B><"flex-col"fS>>' + '<"flex-row"<"flex-col flex-grow"tr>>' + '<"flex-row flex-between"<"flex-col"i><"flex-col"p>>', buttons: { name: 'tableTools', buttons: [ { extend: 'copy', tag: 'a', className: config.moduleHeadlineIconClass, text: ' copy', exportOptions: { orthogonal: 'export' } }, { extend: 'csv', tag: 'a', className: config.moduleHeadlineIconClass, text: ' csv', exportOptions: { orthogonal: 'export' } } ] }, paging: true, ordering: true, order: [1, 'desc'], hover: false, pageLength: 10, lengthMenu: [[5, 10, 25, 50, 100, -1], [5, 10, 25, 50, 100, 'All']], data: logData, // load cached logs (if available) language: { emptyTable: 'No entries', zeroRecords: 'No entries found', lengthMenu: 'Show _MENU_', info: 'Showing _START_ to _END_ of _TOTAL_ entries' }, columnDefs: [ { targets: 0, name: 'status', title: '', width: 18, searchable: false, class: ['text-center'].join(' '), data: 'status', render: { display: (cellData, type, rowData, meta) => { let statusClass = Util.getLogInfo(cellData, 'class'); return ''; } } },{ targets: 1, name: 'time', title: '', width: 50, searchable: true, class: 'text-right', data: 'timestamp', render: { display: (cellData, type, rowData, meta) => rowData.timestampFormatted } },{ targets: 2, name: 'duration', title: '', width: 35, searchable: false, class: 'text-right', data: 'duration', render: { display: (cellData, type, rowData, meta) => { let logStatus = getLogStatusByDuration(rowData.key, cellData); let statusClass = Util.getLogInfo(logStatus, 'class'); return '' + cellData + 'ms'; } } },{ targets: 3, name: 'description', title: 'description', searchable: true, data: 'description' },{ targets: 4, name: 'logType', title: 'type', width: 40, searchable: true, class: ['text-center'].join(' '), data: 'logType', render: { display: (cellData, type, rowData, meta) => { let typeIconClass = getLogTypeIconClass(cellData); return ''; } } },{ targets: 5, name: 'process', title: 'Prozess-ID', width: 80, searchable: false, class: 'text-right', data: 'key' } ] }); // open dialog let logDialog = bootbox.dialog({ title: 'Task-Manager', message: contentTaskManager, size: 'large', buttons: { close: { label: 'close', className: 'btn-default' } } }); // modal dialog is shown logDialog.on('shown.bs.modal', function(e){ updateSyncStatus(); // show Morris graphs ---------------------------------------------------------- let labelYFormat = val => Math.round(val) + 'ms'; for(let key in chartData){ if(chartData.hasOwnProperty(key)){ // create a chart for each key let colElementGraph = $('
', { class: ['col-md-6'].join(' ') }); // graph element let graphElement = $('
', { class: config.logGraphClass }); let graphArea = $('
', { class: Util.config.dynamicAreaClass }).append( graphElement ); let headline = $('

', { text: key }).prepend( $('', { class: ['txt-color', 'txt-color-grayLight'].join(' '), text: 'Prozess-ID: ' }) ); // show update ping between function calls let updateElement = $('', { class: ['txt-color', 'txt-color-blue', 'pull-right'].join(' ') }); headline.append(updateElement).append('
'); // show average execution time let averageElement = $('', { class: 'pull-right' }); headline.append(averageElement); colElementGraph.append(headline); colElementGraph.append(graphArea); graphArea.showLoadingAnimation(); rowElementGraphs.append( colElementGraph ); // cache DOM Elements that will be updated frequently chartData[key].averageElement = averageElement; chartData[key].updateElement = updateElement; chartData[key].graph = Morris.Area({ element: graphElement, data: [], xkey: 'x', ykeys: ['y'], labels: [key], lineColors: ['#375959'], lineWidth: 2, pointSize: 3, pointFillColors: ['#477372'], pointStrokeColors: ['#313335'], ymin: 0, smooth: false, hideHover: true, parseTime: false, postUnits: 'ms', yLabelFormat: labelYFormat, goals: [], goalStrokeWidth: 1, goalLineColors: ['#66c84f'], grid: true, gridTextColor: '#63676a', gridTextSize: 9, gridTextFamily: 'Arial, "Oxygen Bold"', gridTextWeight: 'bold', gridStrokeWidth: 0.3, resize: true, // we use our own resize function dataLabels: false, hoverReversed: true, // Area chart specific options behaveLikeLine: true, fillOpacity: 0.5, belowArea: true, areaColors: ['#3c3f41'], // Not documented but working padding: 8, }); updateLogGraph(key); graphArea.hideLoadingAnimation(); } } /* Util.getResizeManager().observe( this.querySelector('.modal-dialog'), (el, contentRect) => Object.values(chartData).forEach(data => { if(data.graph) data.graph.redraw(); }), {debounce: true, ms: 100} );*/ }); // modal dialog before hide logDialog.on('hide.bs.modal', function(e){ Object.entries(chartData).forEach(([key, data]) => { if(data.graph){ data.graph.destroy(); delete chartData[key].graph; } }); // destroy logTable logDataTable.destroy(true); logDataTable= null; // remove event -> prevent calling this multiple times $(this).off('hide.bs.modal'); }); }); }; /** * updates the log graph for a log key * @param key * @param duration (if undefined -> just update graph with current data) */ let updateLogGraph = (key, duration) => { // check if graph data already exist if(!chartData.hasOwnProperty(key)){ chartData[key] = { data: [], graph: null, averageElement: null, updateElement: null }; } // add new value if(duration !== undefined){ chartData[key].data.unshift(duration); } if(chartData[key].data.length > maxGraphDataCount){ chartData[key].data = chartData[key].data.slice(0, maxGraphDataCount); } let getGraphData = data => { let tempChartData = { data: [], dataSum: 0, average: 0 }; for(let x = 0; x < maxGraphDataCount; x++){ let value = 0; if(data[x]){ value = data[x]; tempChartData.dataSum = Number((tempChartData.dataSum + value).toFixed(2)); } tempChartData.data.push({ x: x, y: value }); } // calculate average tempChartData.average = Number((tempChartData.dataSum / data.length).toFixed(2)); return tempChartData; }; let tempChartData = getGraphData(chartData[key].data); // add new data to graph (Morris chart) - if is already initialized if(chartData[key].graph){ let avgElement = chartData[key].averageElement; let updateElement = chartData[key].updateElement; let delay = Util.getCurrentTriggerDelay(key, 0); if(delay){ updateElement[0].textContent = ' delay: ' + delay.toFixed(2) + ' ms'; } // set/change average line chartData[key].graph.options.goals = [tempChartData.average]; // change avg. display avgElement[0].textContent = 'avg. ' + tempChartData.average.toFixed(2) + ' ms'; let avgStatus = getLogStatusByDuration(key, tempChartData.average); let avgStatusClass = Util.getLogInfo( avgStatus, 'class'); //change avg. display class if( !avgElement.hasClass(avgStatusClass) ){ // avg status changed! avgElement.removeClass().addClass('pull-right txt-color ' + avgStatusClass); // change goals line color if(avgStatus === 'warning'){ chartData[key].graph.options.goalLineColors = ['#e28a0d']; $(document).setProgramStatus('slow connection'); }else{ chartData[key].graph.options.goalLineColors = ['#5cb85c']; } } // set new data and redraw chartData[key].graph.setData( tempChartData.data ); } return tempChartData.data; }; /** * get the log "status" by log duration (ms). * If duration > warning limit -> show as warning * @param logKey * @param logDuration * @returns {string} */ let getLogStatusByDuration = (logKey, logDuration) => { let logStatus = 'info'; if(logDuration > Init.timer[logKey].EXECUTION_LIMIT){ logStatus = 'warning'; } return logStatus; }; /** * get the css class for a specific log type * @param logType * @returns {string} */ let getLogTypeIconClass = logType => { let logIconClass = ''; switch(logType){ case 'client': logIconClass = 'fa-user'; break; case 'server': logIconClass = 'fa-download'; break; } return logIconClass; }; /** * init logging -> set global log events */ let init = () => { let maxEntries = 150; $(window).on('pf:syncStatus', () => { updateSyncStatus(); }); // set global logging listener $(window).on('pf:log', (e, logKey, options) => { // check required logging information if( options && options.duration && options.description ){ let logDescription = options.description; let logDuration = options.duration; let logType = options.type; // update graph data updateLogGraph(logKey, logDuration); let time = Util.getServerTime(); let timestamp = time.getTime(); let timestampFormatted = time.toLocaleTimeString('en-US', { hour12: false }); let logRowData = { status: getLogStatusByDuration(logKey, logDuration), timestamp: timestamp, timestampFormatted: timestampFormatted, duration: logDuration, description: logDescription, logType: logType, key: logKey }; if(logDataTable){ // add row if dataTable is initialized before new log logDataTable.row.add( logRowData ).draw(false); }else{ // add row data to cache logData.push(logRowData); } } // delete old log entries from table --------------------------------- if(logData.length >= maxEntries){ if(logDataTable){ logDataTable.rows(0, {order:'index'}).remove().draw(false); }else{ logData.shift(); } } // cache logs in order to keep previous logs in table after reopening the dialog if(logDataTable){ logData = logDataTable.rows({order:'index'}).data(); } }); }; return { init: init, showDialog: showDialog }; });