diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index c18d456..be066de 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -9,9 +9,12 @@ function resolve(dir) { module.exports = { target: 'node', context: resolve('src'), - entry: './module.ts', + entry: { + './module': './module.ts', + './panel/graph_panel/module': './panel/graph_panel/graph_ctrl.ts' + }, output: { - filename: 'module.js', + filename: '[name].js', path: resolve('dist'), libraryTarget: 'amd' }, @@ -31,7 +34,8 @@ module.exports = { new CopyWebpackPlugin([ { from: 'plugin.json' }, { from: 'img/*' }, - { from: 'partials/*' } + { from: 'panel/graph_panel/plugin.json', to: 'panel/graph_panel/plugin.json' }, + { from: 'panel/graph_panel/partials/*' } ]) ], resolve: { diff --git a/src/module.ts b/src/module.ts index fbe999c..8aa0747 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,647 +1 @@ -import './series_overrides_ctrl'; - -import template from './template'; - -import { GraphRenderer } from './graph_renderer'; -import { GraphLegend } from './graph_legend'; -import { DataProcessor } from './data_processor'; -import { MetricExpanded } from './models/metric'; -import { DatasourceRequest } from './models/datasource'; -import { AnalyticUnitId, AnalyticUnit } from './models/analytic_unit'; -import { AnalyticService } from './services/analytic_service'; -import { AnalyticController } from './controllers/analytic_controller'; -import { PanelInfo } from './models/info'; - -import { axesEditorComponent } from './axes_editor'; - -import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; -import { appEvents } from 'grafana/app/core/core' -import { BackendSrv } from 'grafana/app/core/services/backend_srv'; - - -import _ from 'lodash'; - -const BACKEND_VARIABLE_NAME = 'HASTIC_SERVER_URL'; - - -class GraphCtrl extends MetricsPanelCtrl { - static template = template; - - analyticService: AnalyticService; - hiddenSeries: any = {}; - seriesList: any = []; - dataList: any = []; - // annotations: any = []; - - private _datasourceRequest: DatasourceRequest; - private _datasources: any; - - private _panelPath: any; - private _renderError: boolean = false; - - // annotationsPromise: any; - dataWarning: any; - colors: any = []; - subTabIndex: number; - processor: DataProcessor; - - analyticsController: AnalyticController; - - _graphRenderer: GraphRenderer; - _graphLegend: GraphLegend; - - _panelInfo: PanelInfo; - - private _analyticUnitTypes: any; - - panelDefaults = { - // datasource name, null = default datasource - datasource: null, - // sets client side (flot) or native graphite png renderer (png) - renderer: 'flot', - yaxes: [ - { - label: null, - show: true, - logBase: 1, - min: null, - max: null, - format: 'short', - }, - { - label: null, - show: true, - logBase: 1, - min: null, - max: null, - format: 'short', - }, - ], - xaxis: { - show: true, - mode: 'time', - name: null, - values: [], - buckets: null, - }, - // show/hide lines - lines: true, - // fill factor - fill: 1, - // line width in pixels - linewidth: 1, - // show/hide dashed line - dashes: false, - // length of a dash - dashLength: 10, - // length of space between two dashes - spaceLength: 10, - // show hide points - points: false, - // point radius in pixels - pointradius: 5, - // show hide bars - bars: false, - // enable/disable stacking - stack: false, - // stack percentage mode - percentage: false, - // legend options - legend: { - show: true, // disable/enable legend - values: false, // disable/enable legend values - min: false, - max: false, - current: false, - total: false, - avg: false, - }, - // how null points should be handled - nullPointMode: 'null', - // staircase line mode - steppedLine: false, - // tooltip options - tooltip: { - value_type: 'individual', - shared: true, - sort: 0, - }, - // time overrides - timeFrom: null, - timeShift: null, - // metric queries - targets: [{}], - // series color overrides - aliasColors: {}, - // other style overrides - seriesOverrides: [], - thresholds: [] - }; - - /** @ngInject */ - constructor( - $scope, $injector, $http, - private annotationsSrv, - private keybindingSrv, - private backendSrv: BackendSrv, - private popoverSrv, - private contextSrv -) { - super($scope, $injector); - - _.defaults(this.panel, this.panelDefaults); - _.defaults(this.panel.tooltip, this.panelDefaults.tooltip); - _.defaults(this.panel.legend, this.panelDefaults.legend); - _.defaults(this.panel.xaxis, this.panelDefaults.xaxis); - - this.processor = new DataProcessor(this.panel); - - this.analyticService = new AnalyticService(this.backendURL, $http); - - this.runBackendConnectivityCheck(); - - this.analyticsController = new AnalyticController(this.panel, this.analyticService, this.events); - - this.bindDKey(); - - this.events.on('render', this.onRender.bind(this)); - this.events.on('data-received', this.onDataReceived.bind(this)); - this.events.on('data-error', this.onDataError.bind(this)); - this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); - this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); - this.events.on('init-panel-actions', this.onInitPanelActions.bind(this)); - - this.events.on('analytic-unit-status-change', async (analyticUnit: AnalyticUnit) => { - if(analyticUnit === undefined) { - throw new Error('analyticUnit is undefined'); - } - if(analyticUnit.status === '404') { - await this.analyticsController.removeAnalyticUnit(analyticUnit.id, true); - } - if(analyticUnit.status === 'READY') { - await this.analyticsController.fetchSegments(analyticUnit, +this.range.from, +this.range.to); - } - this.render(this.seriesList); - this.$scope.$digest(); - }); - - this._datasources = {}; - - appEvents.on('ds-request-response', data => { - let requestConfig = data.config; - - this._datasourceRequest = { - url: requestConfig.url, - method: requestConfig.method, - data: requestConfig.data, - params: requestConfig.params, - type: undefined - }; - }); - - this.analyticsController.fetchAnalyticUnitsStatuses(); - - } - - bindDKey() { - this.keybindingSrv.bind('d', this.onDKey.bind(this)); - } - - editPanel() { - super.editPanel(); - this.bindDKey(); - } - - get backendURL(): string { - if(this.templateSrv.index[BACKEND_VARIABLE_NAME] === undefined) { - return undefined; - } - let val = this.templateSrv.index[BACKEND_VARIABLE_NAME].current.value; - val = val.replace(/\/+$/, ""); - return val; - } - - async updateAnalyticUnitTypes() { - const analyticUnitTypes = await this.analyticService.getAnalyticUnitTypes(); - this._analyticUnitTypes = analyticUnitTypes; - } - - get analyticUnitTypes() { - return this._analyticUnitTypes; - } - - get analyticUnitDetectorTypes() { - return _.keys(this._analyticUnitTypes); - } - - async runBackendConnectivityCheck() { - if(this.backendURL === '' || this.backendURL === undefined) { - appEvents.emit( - 'alert-warning', - [ - `Dashboard variable $${BACKEND_VARIABLE_NAME} is missing`, - `Please set $${BACKEND_VARIABLE_NAME}` - ] - ); - return; - } - - let connected = await this.analyticService.isBackendOk(); - if(connected) { - this.updateAnalyticUnitTypes(); - appEvents.emit( - 'alert-success', - [ - 'Connected to Hastic server', - `Hastic server: "${this.backendURL}"` - ] - ); - } - } - - link(scope, elem, attrs, ctrl) { - var $graphElem = $(elem[0]).find('#graphPanel'); - var $legendElem = $(elem[0]).find('#graphLegend'); - this._graphRenderer = new GraphRenderer( - $graphElem, this.timeSrv, this.popoverSrv, this.contextSrv,this.$scope - ); - this._graphLegend = new GraphLegend($legendElem, this.popoverSrv, this.$scope); - } - - onInitEditMode() { - this._updatePanelInfo(); - this.analyticsController.updateServerInfo(); - - const partialPath = this.panelPath + 'partials'; - this.addEditorTab('Analytics', `${partialPath}/tab_analytics.html`, 2); - this.addEditorTab('Webhooks', `${partialPath}/tab_webhooks.html`, 3); - this.addEditorTab('Axes', axesEditorComponent, 4); - this.addEditorTab('Legend', `${partialPath}/tab_legend.html`, 5); - this.addEditorTab('Display', `${partialPath}/tab_display.html`, 6); - this.addEditorTab('Hastic info', `${partialPath}/tab_info.html`, 7); - - this.subTabIndex = 0; - } - - onInitPanelActions(actions) { - actions.push({ text: 'Export CSV', click: 'ctrl.exportCsv()' }); - actions.push({ text: 'Toggle legend', click: 'ctrl.toggleLegend()' }); - } - - issueQueries(datasource) { - // this.annotationsPromise = this.annotationsSrv.getAnnotations({ - // dashboard: this.dashboard, - // panel: this.panel, - // range: this.range, - // }); - return super.issueQueries(datasource); - } - - zoomOut(evt) { - this.publishAppEvent('zoom-out', 2); - } - - onDataSnapshotLoad(snapshotData) { - // this.annotationsPromise = this.annotationsSrv.getAnnotations({ - // dashboard: this.dashboard, - // panel: this.panel, - // range: this.range, - // }); - this.onDataReceived(snapshotData); - } - - onDataError(err) { - this.seriesList = []; - // this.annotations = []; - this.render([]); - } - - async onDataReceived(dataList) { - - this.dataList = dataList; - this.seriesList = this.processor.getSeriesList({ - dataList: dataList, - range: this.range, - }); - - this.dataWarning = null; - const hasSomePoint = this.seriesList.some(s => s.datapoints.length > 0); - - if (!hasSomePoint) { - this.dataWarning = { - title: 'No data points', - tip: 'No datapoints returned from data query', - }; - } else { - for (let series of this.seriesList) { - if (series.isOutsideRange) { - this.dataWarning = { - title: 'Data points outside time range', - tip: 'Can be caused by timezone mismatch or missing time filter in query', - }; - break; - } - } - } - - var loadTasks = [ - // this.annotationsPromise, - this.analyticsController.fetchAnalyticUnitsSegments(+this.range.from, +this.range.to) - ]; - - var results = await Promise.all(loadTasks); - this.loading = false; - // this.annotations = results[0].annotations; - this.render(this.seriesList); - - } - - onRender(data) { - if (!this.seriesList) { - return; - } - - for (let series of this.seriesList) { - if (series.prediction) { - var overrideItem = _.find( - this.panel.seriesOverrides, - el => el.alias === series.alias - ) - if (overrideItem !== undefined) { - this.addSeriesOverride({ - alias: series.alias, - linewidth: 0, - legend: false, - // if pointradius === 0 -> point still shows, that's why pointradius === -1 - pointradius: -1, - fill: 3 - }); - } - } - series.applySeriesOverrides(this.panel.seriesOverrides); - - if (series.unit) { - this.panel.yaxes[series.yaxis - 1].format = series.unit; - } - } - - if(!this.analyticsController.graphLocked) { - this._graphRenderer.render(data); - this._graphLegend.render(); - this._graphRenderer.renderPanel(); - } - } - - changeSeriesColor(series, color) { - series.color = color; - this.panel.aliasColors[series.alias] = series.color; - this.render(); - } - - toggleSeries(serie, event) { - if (event.ctrlKey || event.metaKey || event.shiftKey) { - if (this.hiddenSeries[serie.alias]) { - delete this.hiddenSeries[serie.alias]; - } else { - this.hiddenSeries[serie.alias] = true; - } - } else { - this.toggleSeriesExclusiveMode(serie); - } - this.render(); - } - - toggleSeriesExclusiveMode(serie) { - var hidden = this.hiddenSeries; - - if (hidden[serie.alias]) { - delete hidden[serie.alias]; - } - - // check if every other series is hidden - var alreadyExclusive = _.every(this.seriesList, value => { - if (value.alias === serie.alias) { - return true; - } - - return hidden[value.alias]; - }); - - if (alreadyExclusive) { - // remove all hidden series - _.each(this.seriesList, value => { - delete this.hiddenSeries[value.alias]; - }); - } else { - // hide all but this serie - _.each(this.seriesList, value => { - if (value.alias === serie.alias) { - return; - } - - this.hiddenSeries[value.alias] = true; - }); - } - } - - toggleAxis(info) { - var override: any = _.find(this.panel.seriesOverrides, { alias: info.alias }); - if (!override) { - override = { alias: info.alias }; - this.panel.seriesOverrides.push(override); - } - info.yaxis = override.yaxis = info.yaxis === 2 ? 1 : 2; - this.render(); - } - - addSeriesOverride(override) { - this.panel.seriesOverrides.push(override || {}); - } - - removeSeriesOverride(override) { - this.panel.seriesOverrides = _.without(this.panel.seriesOverrides, override); - this.render(); - } - - toggleLegend() { - this.panel.legend.show = !this.panel.legend.show; - this.refresh(); - } - - legendValuesOptionChanged() { - var legend = this.panel.legend; - legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total; - this.render(); - } - - exportCsv() { - var scope = this.$scope.$new(true); - scope.seriesList = this.seriesList; - this.publishAppEvent('show-modal', { - templateHtml: '', - scope, - modalClass: 'modal--narrow', - }); - } - - // getAnnotationsByTag(tag) { - // var res = []; - // for (var annotation of this.annotations) { - // if (annotation.tags.indexOf(tag) >= 0) { - // res.push(annotation); - // } - // } - // return res; - // } - - // get annotationTags() { - // var res = []; - // for (var annotation of this.annotations) { - // for (var tag of annotation.tags) { - // if (res.indexOf(tag) < 0) { - // res.push(tag); - // } - // } - // } - // return res; - // } - - get panelPath() { - if (this._panelPath === undefined) { - this._panelPath = 'public/plugins/' + this.pluginId + '/'; - } - return this._panelPath; - } - - createNew() { - this.analyticsController.createNew(); - } - - async saveNew() { - try { - const panelId = this.panel.id; - const panelUrl = window.location.origin + window.location.pathname + `?panelId=${panelId}`; - - const datasource = await this._getDatasourceRequest(); - - await this.analyticsController.saveNew( - new MetricExpanded(this.panel.datasource, this.panel.targets), - datasource, - panelUrl - ); - } catch(e) { - appEvents.emit( - 'alert-error', - [ - 'Error while saving analytic unit', - e.message - ] - ); - } - this.$scope.$digest(); - this.render(this.seriesList); - } - - onAnalyticUnitAlertChange(analyticUnit: AnalyticUnit) { - this.analyticsController.toggleAnalyticUnitAlert(analyticUnit); - } - - onAnalyticUnitNameChange(analyticUnit: AnalyticUnit) { - this.analyticsController.fetchAnalyticUnitName(analyticUnit); - } - - onColorChange(id: AnalyticUnitId, value: string) { - if(id === undefined) { - throw new Error('id is undefined'); - } - this.analyticsController.onAnalyticUnitColorChange(id, value); - this.render(); - } - - async onRemove(id: AnalyticUnitId) { - if(id === undefined) { - throw new Error('id is undefined'); - } - await this.analyticsController.removeAnalyticUnit(id); - this.render(); - } - - onCancelLabeling(id: AnalyticUnitId) { - this.$scope.$root.appEvent('confirm-modal', { - title: 'Clear labeling', - text2: 'Your changes will be lost.', - yesText: 'Clear', - icon: 'fa-warning', - altActionText: 'Save', - onAltAction: () => { - this.onToggleLabelingMode(id); - }, - onConfirm: () => { - this.analyticsController.undoLabeling(); - this.render(); - }, - }); - } - - async onToggleLabelingMode(id: AnalyticUnitId) { - this.refresh(); - const datasource = await this._getDatasourceRequest(); - const metric = new MetricExpanded(this.panel.datasource, this.panel.targets); - await this.analyticsController.toggleUnitTypeLabelingMode(id, metric, datasource); - this.$scope.$digest(); - this.render(); - } - - onDKey() { - if(!this.analyticsController.labelingMode) { - return; - } - this.analyticsController.toggleDeleteMode(); - this.refresh(); - } - - onToggleVisibility(id: AnalyticUnitId) { - this.analyticsController.toggleVisibility(id); - this.render(); - } - - private async _updatePanelInfo() { - const datasource = await this._getDatasourceByName(this.panel.datasource); - - this._panelInfo = { - grafanaVersion: this.contextSrv.version, - grafanaUrl: window.location.host, - datasourceType: datasource.type, - hasticServerUrl: this.backendURL - }; - } - - private async _getDatasourceRequest() { - if(this._datasourceRequest.type === undefined) { - const datasource = await this._getDatasourceByName(this.panel.datasource); - this._datasourceRequest.type = datasource.type; - } - return this._datasourceRequest; - } - - private async _getDatasourceByName(name: string) { - if(name === null) { - throw new Error('Trying to get datasource with NULL name'); - } - if(this._datasources[name] === undefined) { - const datasource = await this.backendSrv.get(`/api/datasources/name/${name}`); - return datasource; - } else { - return this._datasources[name]; - } - } - - get panelInfo() { - return this._panelInfo; - } - - get renderError(): boolean { return this._renderError; } - set renderError(value: boolean) { this._renderError = value; } -} - -export { GraphCtrl, GraphCtrl as PanelCtrl }; +export { }; diff --git a/src/axes_editor.ts b/src/panel/graph_panel/axes_editor.ts similarity index 100% rename from src/axes_editor.ts rename to src/panel/graph_panel/axes_editor.ts diff --git a/src/colors.ts b/src/panel/graph_panel/colors.ts similarity index 100% rename from src/colors.ts rename to src/panel/graph_panel/colors.ts diff --git a/src/controllers/analytic_controller.ts b/src/panel/graph_panel/controllers/analytic_controller.ts similarity index 100% rename from src/controllers/analytic_controller.ts rename to src/panel/graph_panel/controllers/analytic_controller.ts diff --git a/src/data_processor.ts b/src/panel/graph_panel/data_processor.ts similarity index 100% rename from src/data_processor.ts rename to src/panel/graph_panel/data_processor.ts diff --git a/src/panel/graph_panel/graph_ctrl.ts b/src/panel/graph_panel/graph_ctrl.ts new file mode 100644 index 0000000..f7a08cf --- /dev/null +++ b/src/panel/graph_panel/graph_ctrl.ts @@ -0,0 +1,647 @@ +import './series_overrides_ctrl'; + +import template from './template'; + +import { GraphRenderer } from './graph_renderer'; +import { GraphLegend } from './graph_legend'; +import { DataProcessor } from './data_processor'; +import { MetricExpanded } from './models/metric'; +import { DatasourceRequest } from './models/datasource'; +import { AnalyticUnitId, AnalyticUnit } from './models/analytic_unit'; +import { AnalyticService } from './services/analytic_service'; +import { AnalyticController } from './controllers/analytic_controller'; +import { PanelInfo } from './models/info'; + +import { axesEditorComponent } from './axes_editor'; + +import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; +import { appEvents } from 'grafana/app/core/core' +import { BackendSrv } from 'grafana/app/core/services/backend_srv'; + + +import _ from 'lodash'; + +const BACKEND_VARIABLE_NAME = 'HASTIC_SERVER_URL'; + + +class GraphCtrl extends MetricsPanelCtrl { + static template = template; + + analyticService: AnalyticService; + hiddenSeries: any = {}; + seriesList: any = []; + dataList: any = []; + // annotations: any = []; + + private _datasourceRequest: DatasourceRequest; + private _datasources: any; + + private _panelPath: any; + private _renderError: boolean = false; + + // annotationsPromise: any; + dataWarning: any; + colors: any = []; + subTabIndex: number; + processor: DataProcessor; + + analyticsController: AnalyticController; + + _graphRenderer: GraphRenderer; + _graphLegend: GraphLegend; + + _panelInfo: PanelInfo; + + private _analyticUnitTypes: any; + + panelDefaults = { + // datasource name, null = default datasource + datasource: null, + // sets client side (flot) or native graphite png renderer (png) + renderer: 'flot', + yaxes: [ + { + label: null, + show: true, + logBase: 1, + min: null, + max: null, + format: 'short', + }, + { + label: null, + show: true, + logBase: 1, + min: null, + max: null, + format: 'short', + }, + ], + xaxis: { + show: true, + mode: 'time', + name: null, + values: [], + buckets: null, + }, + // show/hide lines + lines: true, + // fill factor + fill: 1, + // line width in pixels + linewidth: 1, + // show/hide dashed line + dashes: false, + // length of a dash + dashLength: 10, + // length of space between two dashes + spaceLength: 10, + // show hide points + points: false, + // point radius in pixels + pointradius: 5, + // show hide bars + bars: false, + // enable/disable stacking + stack: false, + // stack percentage mode + percentage: false, + // legend options + legend: { + show: true, // disable/enable legend + values: false, // disable/enable legend values + min: false, + max: false, + current: false, + total: false, + avg: false, + }, + // how null points should be handled + nullPointMode: 'null', + // staircase line mode + steppedLine: false, + // tooltip options + tooltip: { + value_type: 'individual', + shared: true, + sort: 0, + }, + // time overrides + timeFrom: null, + timeShift: null, + // metric queries + targets: [{}], + // series color overrides + aliasColors: {}, + // other style overrides + seriesOverrides: [], + thresholds: [] + }; + + /** @ngInject */ + constructor( + $scope, $injector, $http, + private annotationsSrv, + private keybindingSrv, + private backendSrv: BackendSrv, + private popoverSrv, + private contextSrv +) { + super($scope, $injector); + + _.defaults(this.panel, this.panelDefaults); + _.defaults(this.panel.tooltip, this.panelDefaults.tooltip); + _.defaults(this.panel.legend, this.panelDefaults.legend); + _.defaults(this.panel.xaxis, this.panelDefaults.xaxis); + + this.processor = new DataProcessor(this.panel); + + this.analyticService = new AnalyticService(this.backendURL, $http); + + this.runBackendConnectivityCheck(); + + this.analyticsController = new AnalyticController(this.panel, this.analyticService, this.events); + + this.bindDKey(); + + this.events.on('render', this.onRender.bind(this)); + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('data-error', this.onDataError.bind(this)); + this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.events.on('init-panel-actions', this.onInitPanelActions.bind(this)); + + this.events.on('analytic-unit-status-change', async (analyticUnit: AnalyticUnit) => { + if(analyticUnit === undefined) { + throw new Error('analyticUnit is undefined'); + } + if(analyticUnit.status === '404') { + await this.analyticsController.removeAnalyticUnit(analyticUnit.id, true); + } + if(analyticUnit.status === 'READY') { + await this.analyticsController.fetchSegments(analyticUnit, +this.range.from, +this.range.to); + } + this.render(this.seriesList); + this.$scope.$digest(); + }); + + this._datasources = {}; + + appEvents.on('ds-request-response', data => { + let requestConfig = data.config; + + this._datasourceRequest = { + url: requestConfig.url, + method: requestConfig.method, + data: requestConfig.data, + params: requestConfig.params, + type: undefined + }; + }); + + this.analyticsController.fetchAnalyticUnitsStatuses(); + + } + + bindDKey() { + this.keybindingSrv.bind('d', this.onDKey.bind(this)); + } + + editPanel() { + super.editPanel(); + this.bindDKey(); + } + + get backendURL(): string { + if(this.templateSrv.index[BACKEND_VARIABLE_NAME] === undefined) { + return undefined; + } + let val = this.templateSrv.index[BACKEND_VARIABLE_NAME].current.value; + val = val.replace(/\/+$/, ""); + return val; + } + + async updateAnalyticUnitTypes() { + const analyticUnitTypes = await this.analyticService.getAnalyticUnitTypes(); + this._analyticUnitTypes = analyticUnitTypes; + } + + get analyticUnitTypes() { + return this._analyticUnitTypes; + } + + get analyticUnitDetectorTypes() { + return _.keys(this._analyticUnitTypes); + } + + async runBackendConnectivityCheck() { + if(this.backendURL === '' || this.backendURL === undefined) { + appEvents.emit( + 'alert-warning', + [ + `Dashboard variable $${BACKEND_VARIABLE_NAME} is missing`, + `Please set $${BACKEND_VARIABLE_NAME}` + ] + ); + return; + } + + let connected = await this.analyticService.isBackendOk(); + if(connected) { + this.updateAnalyticUnitTypes(); + appEvents.emit( + 'alert-success', + [ + 'Connected to Hastic server', + `Hastic server: "${this.backendURL}"` + ] + ); + } + } + + link(scope, elem, attrs, ctrl) { + var $graphElem = $(elem[0]).find('#graphPanel'); + var $legendElem = $(elem[0]).find('#graphLegend'); + this._graphRenderer = new GraphRenderer( + $graphElem, this.timeSrv, this.popoverSrv, this.contextSrv,this.$scope + ); + this._graphLegend = new GraphLegend($legendElem, this.popoverSrv, this.$scope); + } + + onInitEditMode() { + this._updatePanelInfo(); + this.analyticsController.updateServerInfo(); + + const partialPath = this.panelPath + 'partials/graph_panel'; + this.addEditorTab('Analytics', `${partialPath}/tab_analytics.html`, 2); + this.addEditorTab('Webhooks', `${partialPath}/tab_webhooks.html`, 3); + this.addEditorTab('Axes', axesEditorComponent, 4); + this.addEditorTab('Legend', `${partialPath}/tab_legend.html`, 5); + this.addEditorTab('Display', `${partialPath}/tab_display.html`, 6); + this.addEditorTab('Hastic info', `${partialPath}/tab_info.html`, 7); + + this.subTabIndex = 0; + } + + onInitPanelActions(actions) { + actions.push({ text: 'Export CSV', click: 'ctrl.exportCsv()' }); + actions.push({ text: 'Toggle legend', click: 'ctrl.toggleLegend()' }); + } + + issueQueries(datasource) { + // this.annotationsPromise = this.annotationsSrv.getAnnotations({ + // dashboard: this.dashboard, + // panel: this.panel, + // range: this.range, + // }); + return super.issueQueries(datasource); + } + + zoomOut(evt) { + this.publishAppEvent('zoom-out', 2); + } + + onDataSnapshotLoad(snapshotData) { + // this.annotationsPromise = this.annotationsSrv.getAnnotations({ + // dashboard: this.dashboard, + // panel: this.panel, + // range: this.range, + // }); + this.onDataReceived(snapshotData); + } + + onDataError(err) { + this.seriesList = []; + // this.annotations = []; + this.render([]); + } + + async onDataReceived(dataList) { + + this.dataList = dataList; + this.seriesList = this.processor.getSeriesList({ + dataList: dataList, + range: this.range, + }); + + this.dataWarning = null; + const hasSomePoint = this.seriesList.some(s => s.datapoints.length > 0); + + if (!hasSomePoint) { + this.dataWarning = { + title: 'No data points', + tip: 'No datapoints returned from data query', + }; + } else { + for (let series of this.seriesList) { + if (series.isOutsideRange) { + this.dataWarning = { + title: 'Data points outside time range', + tip: 'Can be caused by timezone mismatch or missing time filter in query', + }; + break; + } + } + } + + var loadTasks = [ + // this.annotationsPromise, + this.analyticsController.fetchAnalyticUnitsSegments(+this.range.from, +this.range.to) + ]; + + var results = await Promise.all(loadTasks); + this.loading = false; + // this.annotations = results[0].annotations; + this.render(this.seriesList); + + } + + onRender(data) { + if (!this.seriesList) { + return; + } + + for (let series of this.seriesList) { + if (series.prediction) { + var overrideItem = _.find( + this.panel.seriesOverrides, + el => el.alias === series.alias + ) + if (overrideItem !== undefined) { + this.addSeriesOverride({ + alias: series.alias, + linewidth: 0, + legend: false, + // if pointradius === 0 -> point still shows, that's why pointradius === -1 + pointradius: -1, + fill: 3 + }); + } + } + series.applySeriesOverrides(this.panel.seriesOverrides); + + if (series.unit) { + this.panel.yaxes[series.yaxis - 1].format = series.unit; + } + } + + if(!this.analyticsController.graphLocked) { + this._graphRenderer.render(data); + this._graphLegend.render(); + this._graphRenderer.renderPanel(); + } + } + + changeSeriesColor(series, color) { + series.color = color; + this.panel.aliasColors[series.alias] = series.color; + this.render(); + } + + toggleSeries(serie, event) { + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (this.hiddenSeries[serie.alias]) { + delete this.hiddenSeries[serie.alias]; + } else { + this.hiddenSeries[serie.alias] = true; + } + } else { + this.toggleSeriesExclusiveMode(serie); + } + this.render(); + } + + toggleSeriesExclusiveMode(serie) { + var hidden = this.hiddenSeries; + + if (hidden[serie.alias]) { + delete hidden[serie.alias]; + } + + // check if every other series is hidden + var alreadyExclusive = _.every(this.seriesList, value => { + if (value.alias === serie.alias) { + return true; + } + + return hidden[value.alias]; + }); + + if (alreadyExclusive) { + // remove all hidden series + _.each(this.seriesList, value => { + delete this.hiddenSeries[value.alias]; + }); + } else { + // hide all but this serie + _.each(this.seriesList, value => { + if (value.alias === serie.alias) { + return; + } + + this.hiddenSeries[value.alias] = true; + }); + } + } + + toggleAxis(info) { + var override: any = _.find(this.panel.seriesOverrides, { alias: info.alias }); + if (!override) { + override = { alias: info.alias }; + this.panel.seriesOverrides.push(override); + } + info.yaxis = override.yaxis = info.yaxis === 2 ? 1 : 2; + this.render(); + } + + addSeriesOverride(override) { + this.panel.seriesOverrides.push(override || {}); + } + + removeSeriesOverride(override) { + this.panel.seriesOverrides = _.without(this.panel.seriesOverrides, override); + this.render(); + } + + toggleLegend() { + this.panel.legend.show = !this.panel.legend.show; + this.refresh(); + } + + legendValuesOptionChanged() { + var legend = this.panel.legend; + legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total; + this.render(); + } + + exportCsv() { + var scope = this.$scope.$new(true); + scope.seriesList = this.seriesList; + this.publishAppEvent('show-modal', { + templateHtml: '', + scope, + modalClass: 'modal--narrow', + }); + } + + // getAnnotationsByTag(tag) { + // var res = []; + // for (var annotation of this.annotations) { + // if (annotation.tags.indexOf(tag) >= 0) { + // res.push(annotation); + // } + // } + // return res; + // } + + // get annotationTags() { + // var res = []; + // for (var annotation of this.annotations) { + // for (var tag of annotation.tags) { + // if (res.indexOf(tag) < 0) { + // res.push(tag); + // } + // } + // } + // return res; + // } + + get panelPath() { + if (this._panelPath === undefined) { + this._panelPath = 'public/plugins/' + this.pluginId + '/'; + } + return this._panelPath; + } + + createNew() { + this.analyticsController.createNew(); + } + + async saveNew() { + try { + const panelId = this.panel.id; + const panelUrl = window.location.origin + window.location.pathname + `?panelId=${panelId}`; + + const datasource = await this._getDatasourceRequest(); + + await this.analyticsController.saveNew( + new MetricExpanded(this.panel.datasource, this.panel.targets), + datasource, + panelUrl + ); + } catch(e) { + appEvents.emit( + 'alert-error', + [ + 'Error while saving analytic unit', + e.message + ] + ); + } + this.$scope.$digest(); + this.render(this.seriesList); + } + + onAnalyticUnitAlertChange(analyticUnit: AnalyticUnit) { + this.analyticsController.toggleAnalyticUnitAlert(analyticUnit); + } + + onAnalyticUnitNameChange(analyticUnit: AnalyticUnit) { + this.analyticsController.fetchAnalyticUnitName(analyticUnit); + } + + onColorChange(id: AnalyticUnitId, value: string) { + if(id === undefined) { + throw new Error('id is undefined'); + } + this.analyticsController.onAnalyticUnitColorChange(id, value); + this.render(); + } + + async onRemove(id: AnalyticUnitId) { + if(id === undefined) { + throw new Error('id is undefined'); + } + await this.analyticsController.removeAnalyticUnit(id); + this.render(); + } + + onCancelLabeling(id: AnalyticUnitId) { + this.$scope.$root.appEvent('confirm-modal', { + title: 'Clear labeling', + text2: 'Your changes will be lost.', + yesText: 'Clear', + icon: 'fa-warning', + altActionText: 'Save', + onAltAction: () => { + this.onToggleLabelingMode(id); + }, + onConfirm: () => { + this.analyticsController.undoLabeling(); + this.render(); + }, + }); + } + + async onToggleLabelingMode(id: AnalyticUnitId) { + this.refresh(); + const datasource = await this._getDatasourceRequest(); + const metric = new MetricExpanded(this.panel.datasource, this.panel.targets); + await this.analyticsController.toggleUnitTypeLabelingMode(id, metric, datasource); + this.$scope.$digest(); + this.render(); + } + + onDKey() { + if(!this.analyticsController.labelingMode) { + return; + } + this.analyticsController.toggleDeleteMode(); + this.refresh(); + } + + onToggleVisibility(id: AnalyticUnitId) { + this.analyticsController.toggleVisibility(id); + this.render(); + } + + private async _updatePanelInfo() { + const datasource = await this._getDatasourceByName(this.panel.datasource); + + this._panelInfo = { + grafanaVersion: this.contextSrv.version, + grafanaUrl: window.location.host, + datasourceType: datasource.type, + hasticServerUrl: this.backendURL + }; + } + + private async _getDatasourceRequest() { + if(this._datasourceRequest.type === undefined) { + const datasource = await this._getDatasourceByName(this.panel.datasource); + this._datasourceRequest.type = datasource.type; + } + return this._datasourceRequest; + } + + private async _getDatasourceByName(name: string) { + if(name === null) { + throw new Error('Trying to get datasource with NULL name'); + } + if(this._datasources[name] === undefined) { + const datasource = await this.backendSrv.get(`/api/datasources/name/${name}`); + return datasource; + } else { + return this._datasources[name]; + } + } + + get panelInfo() { + return this._panelInfo; + } + + get renderError(): boolean { return this._renderError; } + set renderError(value: boolean) { this._renderError = value; } +} + +export { GraphCtrl, GraphCtrl as PanelCtrl }; diff --git a/src/graph_legend.ts b/src/panel/graph_panel/graph_legend.ts similarity index 100% rename from src/graph_legend.ts rename to src/panel/graph_panel/graph_legend.ts diff --git a/src/graph_renderer.ts b/src/panel/graph_panel/graph_renderer.ts similarity index 99% rename from src/graph_renderer.ts rename to src/panel/graph_panel/graph_renderer.ts index 7688d20..2ea3b10 100644 --- a/src/graph_renderer.ts +++ b/src/panel/graph_panel/graph_renderer.ts @@ -10,7 +10,7 @@ import { REGION_DELETE_COLOR_DARK } from './controllers/analytic_controller'; -import { GraphCtrl } from './module'; +import { GraphCtrl } from './graph_ctrl'; import 'jquery'; import './vendor/flot/jquery.flot.js'; diff --git a/src/graph_tooltip.ts b/src/panel/graph_panel/graph_tooltip.ts similarity index 99% rename from src/graph_tooltip.ts rename to src/panel/graph_panel/graph_tooltip.ts index 96441b1..ae5028e 100644 --- a/src/graph_tooltip.ts +++ b/src/panel/graph_panel/graph_tooltip.ts @@ -1,4 +1,4 @@ -import { AnalyticSegmentsSearcher } from 'models/analytic_unit'; +import { AnalyticSegmentsSearcher } from './models/analytic_unit'; export class GraphTooltip { diff --git a/src/histogram.ts b/src/panel/graph_panel/histogram.ts similarity index 100% rename from src/histogram.ts rename to src/panel/graph_panel/histogram.ts diff --git a/src/models/analytic_unit.ts b/src/panel/graph_panel/models/analytic_unit.ts similarity index 100% rename from src/models/analytic_unit.ts rename to src/panel/graph_panel/models/analytic_unit.ts diff --git a/src/models/datasource.ts b/src/panel/graph_panel/models/datasource.ts similarity index 100% rename from src/models/datasource.ts rename to src/panel/graph_panel/models/datasource.ts diff --git a/src/models/info.ts b/src/panel/graph_panel/models/info.ts similarity index 100% rename from src/models/info.ts rename to src/panel/graph_panel/models/info.ts diff --git a/src/models/metric.ts b/src/panel/graph_panel/models/metric.ts similarity index 100% rename from src/models/metric.ts rename to src/panel/graph_panel/models/metric.ts diff --git a/src/models/segment.ts b/src/panel/graph_panel/models/segment.ts similarity index 100% rename from src/models/segment.ts rename to src/panel/graph_panel/models/segment.ts diff --git a/src/models/segment_array.ts b/src/panel/graph_panel/models/segment_array.ts similarity index 100% rename from src/models/segment_array.ts rename to src/panel/graph_panel/models/segment_array.ts diff --git a/src/models/segment_set.ts b/src/panel/graph_panel/models/segment_set.ts similarity index 100% rename from src/models/segment_set.ts rename to src/panel/graph_panel/models/segment_set.ts diff --git a/src/models/threshold.ts b/src/panel/graph_panel/models/threshold.ts similarity index 100% rename from src/models/threshold.ts rename to src/panel/graph_panel/models/threshold.ts diff --git a/src/partials/axes_editor.html b/src/panel/graph_panel/partials/axes_editor.html similarity index 100% rename from src/partials/axes_editor.html rename to src/panel/graph_panel/partials/axes_editor.html diff --git a/src/partials/tab_analytics.html b/src/panel/graph_panel/partials/tab_analytics.html similarity index 100% rename from src/partials/tab_analytics.html rename to src/panel/graph_panel/partials/tab_analytics.html diff --git a/src/partials/tab_display.html b/src/panel/graph_panel/partials/tab_display.html similarity index 100% rename from src/partials/tab_display.html rename to src/panel/graph_panel/partials/tab_display.html diff --git a/src/partials/tab_info.html b/src/panel/graph_panel/partials/tab_info.html similarity index 100% rename from src/partials/tab_info.html rename to src/panel/graph_panel/partials/tab_info.html diff --git a/src/partials/tab_legend.html b/src/panel/graph_panel/partials/tab_legend.html similarity index 100% rename from src/partials/tab_legend.html rename to src/panel/graph_panel/partials/tab_legend.html diff --git a/src/partials/tab_webhooks.html b/src/panel/graph_panel/partials/tab_webhooks.html similarity index 100% rename from src/partials/tab_webhooks.html rename to src/panel/graph_panel/partials/tab_webhooks.html diff --git a/src/panel/graph_panel/plugin.json b/src/panel/graph_panel/plugin.json new file mode 100644 index 0000000..ef0e63b --- /dev/null +++ b/src/panel/graph_panel/plugin.json @@ -0,0 +1,11 @@ +{ + "type": "panel", + "name": "Hastic Graph", + "id": "hastic-graph-panel", + "info": { + "logos": { + "small": "../hastic-app/img/icn-graph-panel.png", + "large": "../hastic-app/img/icn-graph-panel.png" + } + } +} \ No newline at end of file diff --git a/src/series_overrides_ctrl.ts b/src/panel/graph_panel/series_overrides_ctrl.ts similarity index 100% rename from src/series_overrides_ctrl.ts rename to src/panel/graph_panel/series_overrides_ctrl.ts diff --git a/src/services/analytic_service.ts b/src/panel/graph_panel/services/analytic_service.ts similarity index 100% rename from src/services/analytic_service.ts rename to src/panel/graph_panel/services/analytic_service.ts diff --git a/src/template.ts b/src/panel/graph_panel/template.ts similarity index 100% rename from src/template.ts rename to src/panel/graph_panel/template.ts diff --git a/src/vendor/flot/jquery.flot.crosshair.js b/src/panel/graph_panel/vendor/flot/jquery.flot.crosshair.js similarity index 100% rename from src/vendor/flot/jquery.flot.crosshair.js rename to src/panel/graph_panel/vendor/flot/jquery.flot.crosshair.js diff --git a/src/vendor/flot/jquery.flot.dashes.js b/src/panel/graph_panel/vendor/flot/jquery.flot.dashes.js similarity index 100% rename from src/vendor/flot/jquery.flot.dashes.js rename to src/panel/graph_panel/vendor/flot/jquery.flot.dashes.js diff --git a/src/vendor/flot/jquery.flot.events.js b/src/panel/graph_panel/vendor/flot/jquery.flot.events.js similarity index 100% rename from src/vendor/flot/jquery.flot.events.js rename to src/panel/graph_panel/vendor/flot/jquery.flot.events.js diff --git a/src/vendor/flot/jquery.flot.fillbelow.js b/src/panel/graph_panel/vendor/flot/jquery.flot.fillbelow.js similarity index 100% rename from src/vendor/flot/jquery.flot.fillbelow.js rename to src/panel/graph_panel/vendor/flot/jquery.flot.fillbelow.js diff --git a/src/vendor/flot/jquery.flot.fillbetween.js b/src/panel/graph_panel/vendor/flot/jquery.flot.fillbetween.js similarity index 100% rename from src/vendor/flot/jquery.flot.fillbetween.js rename to src/panel/graph_panel/vendor/flot/jquery.flot.fillbetween.js diff --git a/src/vendor/flot/jquery.flot.js b/src/panel/graph_panel/vendor/flot/jquery.flot.js similarity index 100% rename from src/vendor/flot/jquery.flot.js rename to src/panel/graph_panel/vendor/flot/jquery.flot.js diff --git a/src/vendor/flot/jquery.flot.selection.js b/src/panel/graph_panel/vendor/flot/jquery.flot.selection.js similarity index 100% rename from src/vendor/flot/jquery.flot.selection.js rename to src/panel/graph_panel/vendor/flot/jquery.flot.selection.js diff --git a/src/vendor/flot/jquery.flot.stack.js b/src/panel/graph_panel/vendor/flot/jquery.flot.stack.js similarity index 100% rename from src/vendor/flot/jquery.flot.stack.js rename to src/panel/graph_panel/vendor/flot/jquery.flot.stack.js diff --git a/src/vendor/flot/jquery.flot.stackpercent.js b/src/panel/graph_panel/vendor/flot/jquery.flot.stackpercent.js similarity index 100% rename from src/vendor/flot/jquery.flot.stackpercent.js rename to src/panel/graph_panel/vendor/flot/jquery.flot.stackpercent.js diff --git a/src/vendor/flot/jquery.flot.time.js b/src/panel/graph_panel/vendor/flot/jquery.flot.time.js similarity index 100% rename from src/vendor/flot/jquery.flot.time.js rename to src/panel/graph_panel/vendor/flot/jquery.flot.time.js diff --git a/src/vendor/grafana/colors.ts b/src/panel/graph_panel/vendor/grafana/colors.ts similarity index 100% rename from src/vendor/grafana/colors.ts rename to src/panel/graph_panel/vendor/grafana/colors.ts diff --git a/src/vendor/grafana/event.ts b/src/panel/graph_panel/vendor/grafana/event.ts similarity index 100% rename from src/vendor/grafana/event.ts rename to src/panel/graph_panel/vendor/grafana/event.ts diff --git a/src/vendor/grafana/event_manager.ts b/src/panel/graph_panel/vendor/grafana/event_manager.ts similarity index 100% rename from src/vendor/grafana/event_manager.ts rename to src/panel/graph_panel/vendor/grafana/event_manager.ts diff --git a/src/vendor/grafana/ticks.ts b/src/panel/graph_panel/vendor/grafana/ticks.ts similarity index 100% rename from src/vendor/grafana/ticks.ts rename to src/panel/graph_panel/vendor/grafana/ticks.ts diff --git a/src/vendor/grafana/time_series2.ts b/src/panel/graph_panel/vendor/grafana/time_series2.ts similarity index 100% rename from src/vendor/grafana/time_series2.ts rename to src/panel/graph_panel/vendor/grafana/time_series2.ts diff --git a/src/plugin.json b/src/plugin.json index 394bca9..605852d 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -1,12 +1,12 @@ { - "type": "panel", - "name": "Hastic Graph", - "id": "hastic-graph-panel", + "type": "app", + "name": "Hastic", + "id": "hastic-app", "info": { "author": { - "name": "Hastic.Core Team", - "url": "https://github.com/hastic/hastic-grafana-graph-panel" + "name": "CorpGlory Inc.", + "url": "https://github.com/hastic/hastic-grafana-app" }, "logos": { "small": "img/icn-graph-panel.png", @@ -14,9 +14,11 @@ }, "version": "0.2.7" }, - + "includes": [ + {"type": "panel", "name": "Hastic Graph Panel"} + ], "dependencies": { - "grafanaVersion": "5.1.x" + "grafanaVersion": "5.4.x" } } diff --git a/tests/analytic_controller.jest.ts b/tests/analytic_controller.jest.ts index 07b7d64..c345acd 100644 --- a/tests/analytic_controller.jest.ts +++ b/tests/analytic_controller.jest.ts @@ -1,6 +1,6 @@ -import { ANALYTIC_UNIT_COLORS } from '../src/colors'; -import { MetricExpanded } from '../src/models/metric'; -import { DatasourceRequest } from '../src/models/datasource'; +import { ANALYTIC_UNIT_COLORS } from '../src/panel/graph_panel/colors'; +import { MetricExpanded } from '../src/panel/graph_panel/models/metric'; +import { DatasourceRequest } from '../src/panel/graph_panel/models/datasource'; import { analyticController } from './setup_tests'; diff --git a/tests/setup_tests.ts b/tests/setup_tests.ts index 02fb8fc..0d55b96 100644 --- a/tests/setup_tests.ts +++ b/tests/setup_tests.ts @@ -1,8 +1,8 @@ -import { AnalyticController } from '../src/controllers/analytic_controller'; -import { AnalyticUnit, AnalyticUnitId } from '../src/models/analytic_unit'; -import { AnalyticService } from '../src/services/analytic_service'; -import { MetricExpanded } from '../src/models/metric'; -import { DatasourceRequest } from '../src/models/datasource'; +import { AnalyticController } from '../src/panel/graph_panel/controllers/analytic_controller'; +import { AnalyticUnit, AnalyticUnitId } from '../src/panel/graph_panel/models/analytic_unit'; +import { AnalyticService } from '../src/panel/graph_panel/services/analytic_service'; +import { MetricExpanded } from '../src/panel/graph_panel/models/metric'; +import { DatasourceRequest } from '../src/panel/graph_panel/models/datasource'; import { Emitter } from 'grafana/app/core/utils/emitter';