Alexey Velikiy
6 years ago
committed by
GitHub
47 changed files with 685 additions and 667 deletions
@ -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: '<export-data-modal data="seriesList"></export-data-modal>', |
||||
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 { }; |
||||
|
@ -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: '<export-data-modal data="seriesList"></export-data-modal>', |
||||
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 }; |
@ -1,4 +1,4 @@
|
||||
import { AnalyticSegmentsSearcher } from 'models/analytic_unit'; |
||||
import { AnalyticSegmentsSearcher } from './models/analytic_unit'; |
||||
|
||||
|
||||
export class GraphTooltip { |
@ -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" |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue