/** * (C) 2013-21 - ntop.org */ 'use strict'; const DEFINED_WIDGETS = {}; /* Used to implement the on click events onto the graph */ const DEFINED_EVENTS = { /* On click event used by the flow analyze section, redirect to the current url + a single filter */ "db_analyze" : function (event, chartContext, config) { const { dataPointIndex } = config; const { filter } = config.w.config; let value; if(config.w.config.filtering_labels) value = config.w.config.filtering_labels[dataPointIndex]; if(filter.length == 0 || value === undefined) return; let status = ntopng_status_manager.get_status(); let filters = status.filters; filters.push({id: filter[0], operator: "eq", value: value}); // notify that filters status is updated ntopng_events_manager.emit_event(ntopng_events.FILTERS_CHANGE, {filters}); }, "none" : function (event, chartContext, config) { return; }, /* Standard on click event, redirect to the url */ "standard" : function (event, chartContext, config) { const { seriesIndex, dataPointIndex } = config; const { series } = config.w.config; if (seriesIndex === -1) return; if (series === undefined) return; const serie = series[seriesIndex]; if (serie.base_url !== undefined) { const default_url = (serie.start_url || '') const search = serie.data[dataPointIndex].meta.url_query; location.href = `${serie.base_url}?${default_url}${search}`; } }, } export const DEFINED_TOOLTIP = { /* On click event used by the flow analyze section, redirect to the current url + a single filter */ "format_bytes" : function(value, { config, seriesIndex, dataPointIndex }) { return NtopUtils.bytesToSize(value); }, "format_pkts" : function(value, { config, seriesIndex, dataPointIndex }) { return NtopUtils.formatPackets(value); }, /* On click event used by the flow analyze section, redirect to the current url + a single filter */ "format_value" : function(value, { config, seriesIndex, dataPointIndex }) { return NtopUtils.formatValue(value); }, "format_multiple_date" : function(value, { config, seriesIndex, dataPointIndex }) { return new Date(value[0]) + " - " + new Date(value[1]) }, /* * This formatter is used by the bubble host map, from the y axis, * used to show the Hosts, with their respective values */ "format_label_from_xy" : function({series, seriesIndex, dataPointIndex, w}) { const serie = w.config.series[seriesIndex]["data"][dataPointIndex]; const x_value = serie["x"]; const y_value = serie["y"]; const host_name = serie["meta"]["label"]; const x_axis_title = w.config.xaxis.title.text; const y_axis_title = w.config.yaxis[0].title.text; return (`
${host_name}
${x_axis_title}: ${x_value}
${y_axis_title}: ${y_value}
`) }, "format_label_from_xname" : function({series, seriesIndex, dataPointIndex, w}) { const serie = w.config.series[seriesIndex]["data"][dataPointIndex]; const name = serie["name"] const y_value = serie["y"]; const host_name = serie["meta"]["label"]; const x_axis_title = w.config.xaxis.title.text; const y_axis_title = w.config.yaxis[0].title.text; return (`
${host_name}
${x_axis_title}: ${name}
${y_axis_title}: ${y_value}
`) }, } /* Standard Formatter */ const DEFAULT_FORMATTER = DEFINED_TOOLTIP["format_value"]; export class WidgetUtils { static registerWidget(widget) { if (widget === null) throw new Error(`The passed widget reference is null!`); if (widget.name in DEFINED_WIDGETS) throw new Error(`The widget ${widget.name} is already defined!`); DEFINED_WIDGETS[widget.name] = widget; } static getWidgetByName(widgetName) { if (widgetName in DEFINED_WIDGETS) { return DEFINED_WIDGETS[widgetName]; } throw new Error(`Widget ${widgetName} not found!`) } } /** * Define a simple wrapper class for the widgets. */ class Widget { constructor(name, datasource = {}, updateTime = 0, additionalParams = {}) { // field containing the data fetched from the datasources provided this._fetchedData = []; this.name = name; // if 0 then don't update the chart automatically, the time // is expressed in milliseconds this._updateTime = updateTime; this._datasource = datasource; this._additionalParams = additionalParams; } /** * Init the widget. */ async init() { // register the widget to the DEFINED_WIDGETS object WidgetUtils.registerWidget(this); this._fetchedData = await this._fetchData(); if (this._updateTime > 0) { setInterval(async () => { await this.update(this._datasource.params); }, this._updateTime); } } /** * Destroy the widget freeing the resources used. */ async destroy() { } /** * Force the widget to reload it's data. */ async destroyAndUpdate(datasourceParams = {}) { await this.destroy(); await this.update(datasourceParams); } async updateByUrl(url) { const u = new URL(`${location.origin}${this._datasource.name}`); let entries = ntopng_url_manager.get_url_entries(url); for (const [key, value] of entries) { u.searchParams.set(key, value); } this._datasource.endpoint = u.pathname + u.search; this._fetchedData = await this._fetchData(); } async update(datasourceParams = {}) { // build the new endpoint const u = new URL(`${location.origin}${this._datasource.name}`); for (const [key, value] of Object.entries(datasourceParams)) { u.searchParams.set(key, value); } this._datasource.endpoint = u.pathname + u.search; this._fetchedData = await this._fetchData(); } /** * For each datasources provided to the constructor, * do a GET request to a REST endpoint. */ async _fetchData() { const req = await fetch(`${http_prefix}${this._datasource.endpoint}`); return await req.json(); } } export class ChartWidget extends Widget { constructor(name, type = 'line', datasource = {}, updateTime = 0, additionalParams = {}) { super(name, datasource, updateTime, additionalParams); this._chartType = type; this._chart = {}; this._$htmlChart = document.querySelector(`#canvas-widget-${name}`); } static registerEventCallback(widgetName, eventName, callback) { setTimeout(async () => { try { const widget = WidgetUtils.getWidgetByName(widgetName); const updatedOptions = { chart: { events: { [eventName]: callback } } }; await widget._chart.updateOptions(updatedOptions); } catch (e) { } }, 1000); } _generateConfig() { const config = { series: [], tooltip: { enabledOnSeries: [0], x: { show: true, format: 'dd/MM/yyyy HH:mm:ss', }, y: { formatter: function(value, { series, seriesIndex, dataPointIndex, w }) { return value; }, }, z: { show: false, } }, chart: { type: this._chartType, events: {}, height: '100%', toolbar: { show: false, } }, xaxis: { labels: { style: { fontSize: '14px', } }, tooltip: { enabled: true, formatter: function(value) { return value; } } }, yaxis: { labels: { style: { fontSize: '14px', } }, tooltip: { enabled: true, formatter: function(value) { return value; } } }, zaxis: { labels: { style: { fontSize: '14px', } }, tooltip: { enabled: true } }, dataLabels: { enabled: true, style: { fontSize: '14px', } }, labels: [], legend: { show: true, fontSize: '14px', position: 'bottom', onItemClick: { toggleDataSeries: true, }, }, plotOptions: { bar: { borderRadius: 4, horizontal: true, } }, noData: { text: 'No Data', align: 'center', verticalAlign: 'middle', style: { fontSize: '24px' } } }; // check if the additionalParams field contains an apex property, // then merge the two configurations giving priority to the custom one if (this._additionalParams && this._additionalParams.apex) { const mergedConfig = Object.assign(config, this._additionalParams.apex); return mergedConfig; } return config; } _buildTooltip(config, rsp) { /* By default the areaChart tooltip[y] is overwritten */ config["tooltip"]["y"] = { formatter: function(value, { series, seriesIndex, dataPointIndex, w }) { return value; } }; /* Changing events if given */ if (rsp['tooltip']) { for (const axis in rsp['tooltip']) { if (axis === "x" || axis === "y" || axis === "z") { const formatter = rsp['tooltip'][axis]['formatter']; if(!config['tooltip'][axis]) config['tooltip'][axis] = {} config['tooltip'][axis]['formatter'] = DEFINED_TOOLTIP[formatter] || NtopUtils[formatter] } } /* Customizable tooltip requested */ if(rsp['tooltip']['custom']) config['tooltip']['custom'] = DEFINED_TOOLTIP[rsp['tooltip']['custom']] || NtopUtils[rsp['tooltip']['custom']] } } _buildAxisFormatter(config, axisName) { const axis = config[axisName]; if (axis === undefined || axis.labels === undefined) return; // enable formatters if (axis.labels.ntop_utils_formatter !== undefined && axis.labels.ntop_utils_formatter !== 'none') { const selectedFormatter = axis.labels.ntop_utils_formatter; if (NtopUtils[selectedFormatter] === undefined) { console.error(`xaxis: Formatting function '${selectedFormatter}' didn't found inside NtopUtils.`); } else { axis.labels.formatter = NtopUtils[selectedFormatter]; } } // enable formatters } _buildDataLabels(config, rsp) { if (rsp["dataLabels"]) { for (const [dataLabelsOpts, data] of Object.entries(rsp["dataLabels"])) { config["dataLabels"][dataLabelsOpts] = data; } } let formatter = config["dataLabels"]["formatter"]; if(formatter && DEFINED_TOOLTIP[formatter]) { config["dataLabels"]["formatter"] = DEFINED_TOOLTIP[formatter]; } } _buildConfig() { const config = this._generateConfig(); const rsp = this._fetchedData.rsp; // add additional params fetched from the datasource const additionals = ['series', 'xaxis', 'yaxis', 'colors', 'labels', 'fill', 'filter', 'filtering_labels']; for (const additional of additionals) { if (rsp[additional] === undefined) continue; if (config[additional] !== undefined) { config[additional] = Object.assign(config[additional], rsp[additional]); } else { config[additional] = rsp[additional]; } } /* Changing events if given */ if (rsp['events']) { /* Just pass a table of events. e.g. { events = { click = "db_analyze", updated = "standard" } }*/ for (const event in rsp['events']) { config['chart']['events'][event] = DEFINED_EVENTS[rsp['events'][event]] } } if (rsp['horizontal_chart'] !== undefined) { config['plotOptions']['bar']['horizontal'] = rsp['horizontal_chart']; } this._buildTooltip(config, rsp) this._buildAxisFormatter(config, 'xaxis'); this._buildAxisFormatter(config, 'yaxis'); this._buildDataLabels(config, rsp); return config; } _initializeChart() { const config = this._buildConfig(); this._chartConfig = config; this._chart = new ApexCharts(this._$htmlChart, this._chartConfig); this._chart.render(); } async init() { await super.init(); this._initializeChart(); } async destroy() { await super.destroy(); this._chart.destroy(); this._chart = null; } async update(datasourceParams = {}) { if(this._chartConfig !== undefined) { if (datasourceParams) { await super.update(datasourceParams); } else { await super.updateByUrl(); } if (this._chart != null) { // expecting that rsp contains an object called series const { colors, series, dataLabels, labels, xaxis, filtering_labels } = this._fetchedData.rsp; // update the colors list this._chartConfig.colors = colors; this._chartConfig.series = series; if(xaxis && xaxis.categories) this._chartConfig.xaxis.categories = xaxis.categories; if(filtering_labels) this._chartConfig.filtering_labels = filtering_labels; if(dataLabels) { let formatter = this._chartConfig.dataLabels.formatter; if(formatter && DEFINED_TOOLTIP[formatter]) this._chartConfig.dataLabels.formatter = DEFINED_TOOLTIP[formatter]; else this._chartConfig.dataLabels.formatter = DEFAULT_FORMATTER; } if(labels) this._chartConfig.labels = labels; this._chart.updateOptions(this._chartConfig, true); } } } async destroyAndUpdate(datasource = {}) { await super.destroyAndUpdate(datasource); this._initializeChart(); } }