Passed
Push — master ( c3d610...926549 )
by Marcel
02:48
created

js/app.js   F

Complexity

Total Complexity 80
Complexity/F 2.42

Size

Lines of Code 577
Function Count 33

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 387
c 0
b 0
f 0
dl 0
loc 577
rs 2
wmc 80
mnd 47
bc 47
fnc 33
bpm 1.4242
cpm 2.4242
noi 2

1 Function

Rating   Name   Duplication   Size   Complexity  
F app.js ➔ dismiss 0 8 76

How to fix   Complexity   

Complexity

Complex classes like js/app.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * Analytics
3
 *
4
 * This file is licensed under the Affero General Public License version 3 or
5
 * later. See the LICENSE.md file.
6
 *
7
 * @author Marcel Scherello <[email protected]>
8
 * @copyright 2020 Marcel Scherello
9
 */
10
/** global: OCA */
11
/** global: OCP */
12
/** global: OC */
13
/** global: table */
14
/** global: Chart */
15
/** global: cloner */
16
/** global: _ */
17
18
'use strict';
19
20
if (!OCA.Analytics) {
21
    /**
22
     * @namespace
23
     */
24
    OCA.Analytics = {
25
        TYPE_EMPTY_GROUP: 0,
26
        TYPE_INTERNAL_FILE: 1,
27
        TYPE_INTERNAL_DB: 2,
28
        TYPE_GIT: 3,
29
        TYPE_EXTERNAL_FILE: 4,
30
        TYPE_EXTERNAL_REGEX: 5,
31
        TYPE_SHARED: 99,
32
        SHARE_TYPE_USER: 0,
33
        SHARE_TYPE_GROUP: 1,
34
        SHARE_TYPE_LINK: 3,
35
        SHARE_TYPE_ROOM: 10,
36
        initialDocumentTitle: null,
37
        currentReportData: {},
38
        // flexible mapping depending on type requiered by the used chart library
39
        chartTypeMapping: {
40
            'datetime': 'line',
41
            'column': 'bar',
42
            'area': 'line',
43
            'line': 'line',
44
            'doughnut': 'doughnut'
45
        },
46
    };
47
}
48
/**
49
 * @namespace OCA.Analytics.Core
50
 */
51
OCA.Analytics.Core = {
52
    initApplication: function () {
53
        const urlHash = decodeURI(location.hash);
54
        if (urlHash.length > 1) {
55
            if (urlHash[2] === 'f') {
56
                window.location.href = '#';
57
                OCA.Analytics.Backend.createDataset(urlHash.substring(3));
58
            } else if (urlHash[2] === 'r') {
59
                OCA.Analytics.Navigation.init(urlHash.substring(4));
60
            }
61
        } else {
62
            OCA.Analytics.Navigation.init();
63
        }
64
    },
65
66
    getDistinctValues: function (array) {
67
        let unique = [];
68
        let distinct = [];
69
        for (let i = 0; i < array.length; i++) {
70
            if (!unique[array[i][0]]) {
71
                distinct.push(array[i][0]);
72
                unique[array[i][0]] = 1;
73
            }
74
        }
75
        return distinct;
76
    },
77
};
78
79
OCA.Analytics.UI = {
80
81
    buildDataTable: function (jsondata) {
82
        document.getElementById('tableContainer').style.removeProperty('display');
83
84
        let columns = [];
85
        let data, unit = '';
86
87
        let header = jsondata.header;
88
        let allDimensions = jsondata.dimensions;
89
        let headerKeys = Object.keys(header);
90
        for (let i = 0; i < headerKeys.length; i++) {
91
            columns[i] = {'title': header[headerKeys[i]]};
92
            let columnType = Object.keys(allDimensions).find(key => allDimensions[key] === header[headerKeys[i]]);
93
94
            if (i === headerKeys.length - 1) {
95
                // prepare for later unit cloumn
96
                //columns[i]['render'] = function(data, type, row, meta) {
97
                //    return data + ' ' + row[row.length-2];
98
                //};
99
                if (header[headerKeys[i]].length === 1) {
100
                    unit = header[headerKeys[i]];
101
                }
102
                columns[i]['render'] = $.fn.dataTable.render.number('.', ',', 2, unit + ' ');
103
                columns[i]['className'] = 'dt-right';
104
            } else if (columnType === 'timestamp') {
105
                columns[i]['render'] = function (data, type, row) {
0 ignored issues
show
Unused Code introduced by
The parameter row is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
106
                    // If display or filter data is requested, format the date
107
                    if (type === 'display' || type === 'filter') {
108
                        return new Date(data * 1000).toLocaleString();
109
                    }
110
                    // Otherwise the data type requested (`type`) is type detection or
111
                    // sorting data, for which we want to use the integer, so just return
112
                    // that, unaltered
113
                    return data;
114
                }
115
            } else if (columnType === 'unit') {
116
                columns[i]['visible'] = false;
117
                columns[i]['searchable'] = false;
118
            }
119
        }
120
        data = jsondata.data;
121
122
        const language = {
123
            search: t('analytics', 'Search'),
124
            lengthMenu: t('analytics', 'Show _MENU_ entries'),
125
            info: t('analytics', 'Showing _START_ to _END_ of _TOTAL_ entries'),
126
            infoEmpty: t('analytics', 'Showing 0 to 0 of 0 entries'),
127
            paginate: {
128
                first: t('analytics', 'first'),
129
                previous: t('analytics', 'previous'),
130
                next: t('analytics', 'next'),
131
                last: t('analytics', 'last')
132
            },
133
        };
134
135
        $('#tableContainer').DataTable({
136
            data: data,
137
            columns: columns,
138
            language: language,
139
            rowCallback: function (row, data, index) {
140
                OCA.Analytics.UI.dataTableRowCallback(row, data, index, jsondata.thresholds)
141
            },
142
        });
143
    },
144
145
    dataTableRowCallback: function (row, data, index, thresholds) {
146
        const operators = {
147
            '=': function (a, b) {
148
                return a === b
149
            },
150
            '<': function (a, b) {
151
                return a < b
152
            },
153
            '>': function (a, b) {
154
                return a > b
155
            },
156
            '<=': function (a, b) {
157
                return a <= b
158
            },
159
            '>=': function (a, b) {
160
                return a >= b
161
            },
162
            '!=': function (a, b) {
163
                return a !== b
164
            },
165
        };
166
167
        thresholds = thresholds.filter(p => p.dimension1 === data[0] || p.dimension1 === '*');
168
169
        for (let threshold of thresholds) {
170
            const comparison = operators[threshold['option']](parseFloat(data[2]), parseFloat(threshold['value']));
171
            threshold['severity'] = parseInt(threshold['severity']);
172
            if (comparison === true) {
173
                if (threshold['severity'] === 2) {
174
                    $(row).find('td:eq(2)').css('color', 'red');
175
                } else if (threshold['severity'] === 3) {
176
                    $(row).find('td:eq(2)').css('color', 'orange');
177
                } else if (threshold['severity'] === 4) {
178
                    $(row).find('td:eq(2)').css('color', 'green');
179
                }
180
            }
181
        }
182
    },
183
184
    buildChart: function (jsondata) {
185
186
        document.getElementById('chartContainer').style.removeProperty('display');
187
        document.getElementById('chartMenuContainer').style.removeProperty('display');
188
        let ctx = document.getElementById('myChart').getContext('2d');
189
190
        let chartType = jsondata.options.chart;
191
        let datasets = [], xAxisCategories = [];
192
        let lastObject = false;
193
        let dataSeries = -1;
194
        let hidden = false;
195
196
        let header = jsondata.header;
197
        let headerKeys = Object.keys(header).length;
198
        let dataSeriesColumn = headerKeys - 3; //characteristic is taken from the second last column
199
        let characteristicColumn = headerKeys - 2; //characteristic is taken from the second last column
200
        let keyFigureColumn = headerKeys - 1; //key figures is taken from the last column
201
202
        Chart.defaults.global.elements.line.borderWidth = 2;
203
        Chart.defaults.global.elements.line.tension = 0.1;
204
        Chart.defaults.global.elements.line.fill = false;
205
        Chart.defaults.global.elements.point.radius = 1;
206
207
        var chartOptions = {
208
            maintainAspectRatio: false,
209
            responsive: true,
210
            scales: {
211
                yAxes: [{
212
                    id: 'primary',
213
                    stacked: false,
214
                    position: 'left',
215
                    display: true,
216
                    gridLines: {
217
                        display: true,
218
                    },
219
                    ticks: {
220
                        callback: function (value) {
221
                            return value.toLocaleString();
222
                        },
223
                    },
224
                }, {
225
                    id: 'secondary',
226
                    stacked: false,
227
                    position: 'right',
228
                    display: false,
229
                    gridLines: {
230
                        display: false,
231
                    },
232
                }],
233
                xAxes: [{
234
                    type: 'category',
235
                    distribution: 'linear',
236
                    gridLines: {
237
                        display: false
238
                    },
239
                    display: true,
240
                }],
241
            },
242
            plugins: {
243
                colorschemes: {
244
                    scheme: 'tableau.ClassicLight10'
245
                }
246
            },
247
            legend: {
248
                display: false,
249
                position: 'bottom'
250
            },
251
            animation: {
252
                duration: 0 // general animation time
253
            },
254
            tooltips: {
255
                callbacks: {
256
                    label: function (tooltipItem, data) {
257
                        let datasetLabel = data.datasets[tooltipItem.datasetIndex].label || '';
258
                        return datasetLabel + ': ' + parseInt(tooltipItem.yLabel).toLocaleString();
259
                    }
260
                }
261
            },
262
        };
263
264
        for (let values of jsondata.data) {
265
            if (dataSeriesColumn >= 0 && lastObject !== values[dataSeriesColumn]) {
266
                // create new dataseries for every new lable in dataSeriesColumn
267
                datasets.push({label: values[dataSeriesColumn], data: [], hidden: hidden});
268
                dataSeries++;
269
                // default hide > 4th series for better visibility
270
                if (dataSeries === 3) {
271
                    hidden = true;
272
                }
273
                lastObject = values[dataSeriesColumn];
274
            } else if (lastObject === false) {
275
                // when only 2 columns are provided, no label will be set
276
                datasets.push({label: '', data: [], hidden: hidden});
277
                dataSeries++;
278
                lastObject = true;
279
            }
280
281
            if (chartType === 'datetime' || chartType === 'area') {
282
                datasets[dataSeries]['data'].push({
283
                    t: values[characteristicColumn],
284
                    y: parseFloat(values[keyFigureColumn])
285
                });
286
            } else {
287
                datasets[dataSeries]['data'].push(parseFloat(values[keyFigureColumn]));
288
                if (dataSeries === 0) {
289
                    // Add category lables only once and not for every data series.
290
                    // They have to be unique anyway
291
                    xAxisCategories.push(values[characteristicColumn]);
292
                }
293
            }
294
        }
295
        if (chartType === 'datetime') {
296
            chartOptions.scales.xAxes[0].type = 'time';
297
            chartOptions.scales.xAxes[0].distribution = 'linear';
298
        } else if (chartType === 'area') {
299
            chartOptions.scales.xAxes[0].type = 'time';
300
            chartOptions.scales.xAxes[0].distribution = 'linear';
301
            chartOptions.scales.yAxes[0].stacked = true;
302
            Chart.defaults.global.elements.line.fill = true;
303
        } else if (chartType === 'doughnut') {
304
            chartOptions.scales.xAxes[0].display = false;
305
            chartOptions.scales.yAxes[0].display = false;
306
            chartOptions.scales.yAxes[0].gridLines.display = false;
307
            chartOptions.scales.yAxes[1].display = false;
308
            chartOptions.scales.yAxes[1].gridLines.display = false;
309
            chartOptions.circumference = Math.PI;
310
            chartOptions.rotation = -Math.PI;
311
            chartOptions.legend.display = true;
312
        }
313
314
        // the user can add/overwrite chart options
315
        // the user can put the options in array-format into the report definition
316
        // these are merged with the standard report settings
317
        // e.g. the display unit for the x-axis can be overwritten '{"scales": {"xAxes": [{"time": {"unit" : "month"}}]}}'
318
        // e.g. add a secondary y-axis '{"scales": {"yAxes": [{},{"id":"B","position":"right"}]}}'
319
        let userChartOptions = jsondata.options.chartoptions;
320
        if (userChartOptions !== '' && userChartOptions !== null) {
321
            chartOptions = cloner.deep.merge(chartOptions, JSON.parse(userChartOptions));
322
        }
323
324
        // the user can modify dataset/series settings
325
        // these are merged with the data array coming from the backend
326
        // e.g. assign one series to the secondary y-axis: '[{"yAxisID":"B"},{},{"yAxisID":"B"},{}]'
327
        //let userDatasetOptions = document.getElementById('userDatasetOptions').value;
328
        let userDatasetOptions = jsondata.options.dataoptions;
329
        if (userDatasetOptions !== '' && userDatasetOptions !== null) {
330
            datasets = cloner.deep.merge(JSON.parse(userDatasetOptions), datasets);
331
        }
332
333
        let myChart = new Chart(ctx, {
334
            type: OCA.Analytics.chartTypeMapping[chartType],
335
            data: {
336
                labels: xAxisCategories,
337
                datasets: datasets
338
            },
339
            options: chartOptions,
340
        });
341
342
        document.getElementById('chartLegend').addEventListener('click', function () {
343
            myChart.options.legend.display = !myChart.options.legend.display;
344
            myChart.update();
345
        });
346
    },
347
348
    resetContent: function () {
349
        if (document.getElementById('advanced').value === 'true') {
350
            document.getElementById('analytics-intro').classList.remove('hidden');
351
            document.getElementById('app-sidebar').classList.add('disappear');
352
        } else {
353
            if ($.fn.dataTable.isDataTable('#tableContainer')) {
354
                $('#tableContainer').DataTable().destroy();
355
            }
356
            document.getElementById('chartMenuContainer').style.display = 'none';
357
            document.getElementById('chartContainer').style.display = 'none';
358
            document.getElementById('chartContainer').innerHTML = '';
359
            document.getElementById('chartContainer').innerHTML = '<canvas id="myChart" ></canvas>';
360
            document.getElementById('tableContainer').style.display = 'none';
361
            document.getElementById('tableContainer').innerHTML = '';
362
            document.getElementById('reportHeader').innerHTML = '';
363
            document.getElementById('reportSubHeader').innerHTML = '';
364
            document.getElementById('reportSubHeader').style.display = 'none';
365
            document.getElementById('filterContainer').style.display = 'none';
366
            document.getElementById('optionsIcon').style.removeProperty('display');
367
            document.getElementById('noDataContainer').style.display = 'none';
368
        }
369
    },
370
371
    notification: function (type, message) {
372
        if (parseInt(OC.config.versionstring.substr(0, 2)) >= 17) {
373
            if (type === 'success') {
374
                OCP.Toast.success(message)
375
            } else if (type === 'error') {
376
                OCP.Toast.error(message)
377
            } else {
378
                OCP.Toast.info(message)
379
            }
380
        } else {
381
            OC.Notification.showTemporary(message);
382
        }
383
    },
384
385
    whatsNewSuccess: function (data, statusText, xhr) {
386
        if (xhr.status !== 200) {
387
            return
388
        }
389
390
        let item, menuItem, text, icon
391
392
        const div = document.createElement('div')
393
        div.classList.add('popovermenu', 'open', 'whatsNewPopover', 'menu-left')
394
395
        const list = document.createElement('ul')
396
397
        // header
398
        item = document.createElement('li')
399
        menuItem = document.createElement('span')
400
        menuItem.className = 'menuitem'
401
402
        text = document.createElement('span')
403
        text.innerText = t('core', 'New in') + ' ' + data['product']
404
        text.className = 'caption'
405
        menuItem.appendChild(text)
406
407
        icon = document.createElement('span')
408
        icon.className = 'icon-close'
409
        icon.onclick = function () {
410
            OCA.Analytics.Backend.whatsnewDismiss(data['version'])
411
        }
412
        menuItem.appendChild(icon)
413
414
        item.appendChild(menuItem)
415
        list.appendChild(item)
416
417
        // Highlights
418
        for (const i in data['whatsNew']['regular']) {
419
            const whatsNewTextItem = data['whatsNew']['regular'][i]
420
            item = document.createElement('li')
421
422
            menuItem = document.createElement('span')
423
            menuItem.className = 'menuitem'
424
425
            icon = document.createElement('span')
426
            icon.className = 'icon-checkmark'
427
            menuItem.appendChild(icon)
428
429
            text = document.createElement('p')
430
            text.innerHTML = _.escape(whatsNewTextItem)
431
            menuItem.appendChild(text)
432
433
            item.appendChild(menuItem)
434
            list.appendChild(item)
435
        }
436
437
        // Changelog URL
438
        if (!_.isUndefined(data['changelogURL'])) {
439
            item = document.createElement('li')
440
441
            menuItem = document.createElement('a')
442
            menuItem.href = data['changelogURL']
443
            menuItem.rel = 'noreferrer noopener'
444
            menuItem.target = '_blank'
445
446
            icon = document.createElement('span')
447
            icon.className = 'icon-link'
448
            menuItem.appendChild(icon)
449
450
            text = document.createElement('span')
451
            text.innerText = t('core', 'View changelog')
452
            menuItem.appendChild(text)
453
454
            item.appendChild(menuItem)
455
            list.appendChild(item)
456
        }
457
458
        div.appendChild(list)
459
        document.body.appendChild(div)
460
    }
461
};
462
463
OCA.Analytics.Backend = {
464
465
    getData: function () {
466
        OCA.Analytics.UI.resetContent();
467
        document.getElementById('analytics-intro').classList.add('hidden');
468
        document.getElementById('analytics-content').removeAttribute('hidden');
469
470
        let url;
471
        if (document.getElementById('sharingToken').value === '') {
472
            const datasetId = document.querySelector('#navigationDatasets .active').dataset.id;
473
            url = OC.generateUrl('apps/analytics/data/') + datasetId;
474
        } else {
475
            const token = document.getElementById('sharingToken').value;
476
            url = OC.generateUrl('apps/analytics/data/public/') + token;
477
        }
478
479
        $.ajax({
480
            type: 'GET',
481
            url: url,
482
            data: {},
483
            success: function (data) {
484
                OCA.Analytics.currentReportData = data;
485
                try {
486
                    OCA.Analytics.currentReportData.options.filteroptions = JSON.parse(OCA.Analytics.currentReportData.options.filteroptions);
487
                } catch (e) {
488
                    OCA.Analytics.currentReportData.options.filteroptions = {};
489
                }
490
                if (OCA.Analytics.currentReportData.options.filteroptions === null) OCA.Analytics.currentReportData.options.filteroptions = {};
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
491
492
                document.getElementById('reportHeader').innerText = data.options.name;
493
494
                if (data.options.subheader !== '') {
495
                    document.getElementById('reportSubHeader').innerText = data.options.subheader;
496
                    document.getElementById('reportSubHeader').style.removeProperty('display');
497
                }
498
                if (parseInt(data.options.type) === OCA.Analytics.TYPE_INTERNAL_DB && document.getElementById('sharingToken').value === '') {
499
                    document.getElementById('filterContainer').style.removeProperty('display');
500
                    OCA.Analytics.Filter.refreshFilterVisualisation();
501
                }
502
                document.title = data.options.name + ' @ ' + OCA.Analytics.initialDocumentTitle;
503
                if (data.status !== 'nodata') {
504
505
                    let visualization = data.options.visualization;
506
                    if (visualization === 'chart') {
507
                        OCA.Analytics.UI.buildChart(data);
508
                    } else if (visualization === 'table') {
509
                        document.getElementById('optionsIcon').style.display = 'none';
510
                        OCA.Analytics.UI.buildDataTable(data);
511
                    } else {
512
                        OCA.Analytics.UI.buildChart(data);
513
                        OCA.Analytics.UI.buildDataTable(data);
514
                    }
515
                } else {
516
                    document.getElementById('noDataContainer').style.removeProperty('display');
517
                }
518
            }
519
        });
520
    },
521
522
    getDatasets: function (datasetId) {
523
        $.ajax({
524
            type: 'GET',
525
            url: OC.generateUrl('apps/analytics/dataset'),
526
            success: function (data) {
527
                OCA.Analytics.Navigation.buildNavigation(data);
528
                OCA.Analytics.Sidebar.Dataset.fillSidebarParentDropdown(data);
529
                if (datasetId) {
530
                    OCA.Analytics.Sidebar.hideSidebar();
531
                    document.querySelector('#navigationDatasets [data-id="' + datasetId + '"]').click();
532
                }
533
            }
534
        });
535
    },
536
537
    createDataset: function (file = '') {
538
        $.ajax({
539
            type: 'POST',
540
            url: OC.generateUrl('apps/analytics/dataset'),
541
            data: {
542
                'file': file,
543
            },
544
            success: function (data) {
545
                OCA.Analytics.Navigation.init(data);
546
            }
547
        });
548
    },
549
550
    whatsnew: function (options) {
551
        options = options || {}
552
        $.ajax({
553
            type: 'GET',
554
            url: OC.generateUrl('apps/analytics/whatsnew'),
555
            data: {'format': 'json'},
556
            success: options.success || function (data, statusText, xhr) {
557
                OCA.Analytics.UI.whatsNewSuccess(data, statusText, xhr)
558
            },
559
        });
560
    },
561
562
    whatsnewDismiss: function dismiss(version) {
563
        $.ajax({
564
            type: 'POST',
565
            url: OC.generateUrl('apps/analytics/whatsnew'),
566
            data: {version: encodeURIComponent(version)}
567
        })
568
        $('.whatsNewPopover').remove()
569
    }
570
};
571
572
document.addEventListener('DOMContentLoaded', function () {
573
    OCA.Analytics.initialDocumentTitle = document.title;
574
    document.getElementById('analytics-warning').classList.add('hidden');
575
576
    if (document.getElementById('sharingToken').value === '') {
577
        OCA.Analytics.Backend.whatsnew();
578
        document.getElementById('analytics-intro').attributes.removeNamedItem('hidden');
579
        OCA.Analytics.Core.initApplication();
580
        document.getElementById('newDatasetButton').addEventListener('click', OCA.Analytics.Navigation.handleNewDatasetButton);
581
        if (document.getElementById('advanced').value === 'false') {
582
            document.getElementById('createDemoReport').addEventListener('click', OCA.Analytics.Navigation.createDemoReport);
583
            document.getElementById('addFilterIcon').addEventListener('click', OCA.Analytics.Filter.openFilterDialog);
584
            document.getElementById('drilldownIcon').addEventListener('click', OCA.Analytics.Filter.openDrilldownDialog);
585
            document.getElementById('optionsIcon').addEventListener('click', OCA.Analytics.Filter.openOptionsDialog);
586
        }
587
    } else {
588
        OCA.Analytics.Backend.getData();
589
    }
590
591
    window.addEventListener("beforeprint", function () {
592
        document.getElementById('chartContainer').style.height = document.getElementById('myChart').style.height;
593
    });
594
});
595