Browse Source

Merge branch 'master' of github.com:hastic/hastic-grafana-app

master
Coin de Gamma 5 years ago
parent
commit
125e3bcfc7
  1. 4
      README.md
  2. 2
      package.json
  3. 156
      src/panel/graph_panel/controllers/analytic_controller.ts
  4. 53
      src/panel/graph_panel/graph_ctrl.ts
  5. 2
      src/panel/graph_panel/graph_legend.ts
  6. 4
      src/panel/graph_panel/graph_renderer.ts
  7. 27
      src/panel/graph_panel/models/analytic_units/analytic_unit.ts
  8. 49
      src/panel/graph_panel/models/analytic_units/anomaly_analytic_unit.ts
  9. 12
      src/panel/graph_panel/models/analytic_units/pattern_analytic_unit.ts
  10. 6
      src/panel/graph_panel/models/analytic_units/threshold_analytic_unit.ts
  11. 6
      src/panel/graph_panel/models/detection.ts
  12. 442
      src/panel/graph_panel/partials/tab_analytics.html
  13. 2
      src/panel/graph_panel/series_overrides_ctrl.ts
  14. 17
      src/panel/graph_panel/services/analytic_service.ts
  15. 2
      src/plugin.json
  16. 2
      src/utlis.ts

4
README.md

@ -21,13 +21,13 @@ See also:
* [Wiki](https://github.com/hastic/hastic-grafana-app/wiki) * [Wiki](https://github.com/hastic/hastic-grafana-app/wiki)
* [FAQ](https://github.com/hastic/hastic-grafana-app/wiki/FAQ) * [FAQ](https://github.com/hastic/hastic-grafana-app/wiki/FAQ)
* [Hastic-server](https://github.com/hastic/hastic-server) * [Hastic-server](https://github.com/hastic/hastic-server)
* [Installation from source](https://github.com/hastic/hastic-grafana-app/wiki/Installation-from-source) * [Install from source](https://github.com/hastic/hastic-grafana-app/wiki/Development#install-from-source)
* [Changelog](https://github.com/hastic/hastic-grafana-app/wiki/Changelog) * [Changelog](https://github.com/hastic/hastic-grafana-app/wiki/Changelog)
# Prerequisites # Prerequisites
* [hastic-server](https://github.com/hastic/hastic-server) * [hastic-server](https://github.com/hastic/hastic-server)
* [Grafana >= 5.4.0](https://grafana.com/grafana/download) * [Grafana >= 5.4.0](https://grafana.com/grafana/download), we don't support Grafana 6.x.x yet
## Support and Consulting ## Support and Consulting

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "grafana-hastic-app", "name": "grafana-hastic-app",
"version": "0.3.3", "version": "0.3.4",
"description": "Hastic app: labeling and rendeting analytics from hastic-server", "description": "Hastic app: labeling and rendeting analytics from hastic-server",
"main": "dist/module", "main": "dist/module",
"scripts": { "scripts": {

156
src/panel/graph_panel/controllers/analytic_controller.ts

@ -1,12 +1,11 @@
// Corresponds to https://github.com/hastic/hastic-server/blob/master/server/src/models/analytic_units/analytic_unit.ts // Corresponds to https://github.com/hastic/hastic-server/blob/master/server/src/models/analytic_units/analytic_unit.ts
import { AnalyticService } from '../services/analytic_service'; import { AnalyticService, TableTimeSeries } from '../services/analytic_service';
import { import {
AnalyticUnitId, AnalyticUnit, AnalyticUnitId, AnalyticUnit,
AnalyticSegment, AnalyticSegmentsSearcher, AnalyticSegmentPair, AnalyticSegment, AnalyticSegmentsSearcher, AnalyticSegmentPair,
LabelingMode, LabelingMode
DetectorType
} from '../models/analytic_units/analytic_unit'; } from '../models/analytic_units/analytic_unit';
import { AnomalyAnalyticUnit } from '../models/analytic_units/anomaly_analytic_unit'; import { AnomalyAnalyticUnit } from '../models/analytic_units/anomaly_analytic_unit';
import { AnalyticUnitsSet } from '../models/analytic_units/analytic_units_set'; import { AnalyticUnitsSet } from '../models/analytic_units/analytic_units_set';
@ -104,9 +103,14 @@ export class AnalyticController {
} }
} }
cancelCreation() {
delete this._newAnalyticUnit;
this._creatingNewAnalyticUnit = false;
}
async saveNew(metric: MetricExpanded, datasource: DatasourceRequest) { async saveNew(metric: MetricExpanded, datasource: DatasourceRequest) {
this._savingNewAnalyticUnit = true; this._savingNewAnalyticUnit = true;
const newAnalyticUnit = createAnalyticUnit(this._newAnalyticUnit.serverObject); const newAnalyticUnit = createAnalyticUnit(this._newAnalyticUnit.toJSON());
newAnalyticUnit.id = await this._analyticService.postNewAnalyticUnit( newAnalyticUnit.id = await this._analyticService.postNewAnalyticUnit(
newAnalyticUnit, metric, datasource, this._grafanaUrl, this._panelId newAnalyticUnit, metric, datasource, this._grafanaUrl, this._panelId
); );
@ -143,7 +147,8 @@ export class AnalyticController {
await this.disableLabeling(); await this.disableLabeling();
this._selectedAnalyticUnitId = id; this._selectedAnalyticUnitId = id;
this.labelingUnit.selected = true; this.labelingUnit.selected = true;
this.toggleLabelingMode(LabelingMode.LABELING); const labelingModes = this.labelingUnit.labelingModes;
this.toggleLabelingMode(labelingModes[0].value);
} }
async disableLabeling() { async disableLabeling() {
@ -196,9 +201,9 @@ export class AnalyticController {
this.labelingUnit.labelingMode = labelingMode; this.labelingUnit.labelingMode = labelingMode;
} }
addLabelSegment(segment: Segment, deleted = false) { addSegment(segment: Segment, deleted = false) {
const asegment = this.labelingUnit.addLabeledSegment(segment, deleted); const addedSegment = this.labelingUnit.addSegment(segment, deleted);
this._labelingDataAddedSegments.addSegment(asegment); this._labelingDataAddedSegments.addSegment(addedSegment);
} }
get analyticUnits(): AnalyticUnit[] { get analyticUnits(): AnalyticUnit[] {
@ -215,7 +220,7 @@ export class AnalyticController {
} else { } else {
analyticUnit.labeledColor = value; analyticUnit.labeledColor = value;
} }
await this.saveAnalyticUnit(analyticUnit); analyticUnit.changed = true;
} }
fetchAnalyticUnitsStatuses() { fetchAnalyticUnitsStatuses() {
@ -309,7 +314,7 @@ export class AnalyticController {
return newIds; return newIds;
} }
async redetectAll() { async redetectAll(from?: number, to?: number) {
this.analyticUnits.forEach(unit => { this.analyticUnits.forEach(unit => {
// TODO: remove duplication with runDetect // TODO: remove duplication with runDetect
unit.segments.clear(); unit.segments.clear();
@ -317,9 +322,9 @@ export class AnalyticController {
unit.status = null; unit.status = null;
}); });
const ids = this.analyticUnits.map(analyticUnit => analyticUnit.id); const ids = this.analyticUnits.map(analyticUnit => analyticUnit.id);
await this._analyticService.runDetect(ids); await this._analyticService.runDetect(ids, from, to);
_.each(this.analyticUnits, analyticUnit => this._runStatusWaiter(analyticUnit)); this.fetchAnalyticUnitsStatuses();
} }
async runDetect(analyticUnitId: AnalyticUnitId, from?: number, to?: number) { async runDetect(analyticUnitId: AnalyticUnitId, from?: number, to?: number) {
@ -453,11 +458,7 @@ export class AnalyticController {
if(!this.inLabelingMode) { if(!this.inLabelingMode) {
throw new Error(`Can't enter ${labelingMode} mode when labeling mode is disabled`); throw new Error(`Can't enter ${labelingMode} mode when labeling mode is disabled`);
} }
if(this.labelingUnit.labelingMode === labelingMode) { this.labelingUnit.labelingMode = labelingMode;
this.labelingUnit.labelingMode = LabelingMode.LABELING;
} else {
this.labelingUnit.labelingMode = labelingMode;
}
} }
async removeAnalyticUnit(id: AnalyticUnitId, silent: boolean = false): Promise<void> { async removeAnalyticUnit(id: AnalyticUnitId, silent: boolean = false): Promise<void> {
@ -478,14 +479,19 @@ export class AnalyticController {
await this._analyticService.setAnalyticUnitAlert(analyticUnit); await this._analyticService.setAnalyticUnitAlert(analyticUnit);
} }
toggleAnalyticUnitChange(analyticUnit: AnalyticUnit, value: boolean): void {
analyticUnit.changed = value;
}
async saveAnalyticUnit(analyticUnit: AnalyticUnit): Promise<void> { async saveAnalyticUnit(analyticUnit: AnalyticUnit): Promise<void> {
if(analyticUnit.id === null || analyticUnit.id === undefined) { if(analyticUnit.id === null || analyticUnit.id === undefined) {
throw new Error('Cannot save analytic unit without id'); throw new Error('Cannot save analytic unit without id');
} }
analyticUnit.saving = true; analyticUnit.saving = true;
await this._analyticService.updateAnalyticUnit(analyticUnit.serverObject); await this._analyticService.updateAnalyticUnit(analyticUnit.toJSON());
analyticUnit.saving = false; analyticUnit.saving = false;
analyticUnit.changed = false;
} }
async getAnalyticUnits(): Promise<any[]> { async getAnalyticUnits(): Promise<any[]> {
@ -503,54 +509,68 @@ export class AnalyticController {
this.fetchAnalyticUnitsStatuses(); this.fetchAnalyticUnitsStatuses();
} }
async getHSR(from: number, to: number): Promise<HSRTimeSeries | null> { async getHSR(from: number, to: number): Promise<{
// Returns HSR (Hastic Signal Representation) for analytic unit with enabled "Show HSR" hsr: HSRTimeSeries,
// Returns null when there is no analytic units which have "Show HSR" enabled lowerBound?: HSRTimeSeries,
if(this.hsrAnalyticUnit === null) { upperBound?: HSRTimeSeries
} | null> {
// Returns HSR (Hastic Signal Representation) for analytic unit in "Inspect" mode
// Returns null when there are no analytic units in "Inspect" mode
// or if there is no response from server
if(this.inspectedAnalyticUnit === null) {
return null; return null;
} }
const hsr = await this._analyticService.getHSR(this.hsrAnalyticUnit.id, from, to); const response = await this._analyticService.getHSR(this.inspectedAnalyticUnit.id, from, to);
const datapoints = hsr.values.map(value => value.reverse() as [number, number]); if(response === null) {
return { target: 'HSR', datapoints }; return null;
}
const hsr = convertTableToTimeSeries('HSR', response.hsr);
const lowerBound = convertTableToTimeSeries('Lower bound', response.lowerBound);
const upperBound = convertTableToTimeSeries('Upper bound', response.upperBound);
return {
hsr,
lowerBound,
upperBound
};
} }
async getHSRSeries(from: number, to: number) { async getHSRSeries(from: number, to: number) {
const hsr = await this.getHSR(from, to); const response = await this.getHSR(from, to);
if(hsr === null) { if(response === null) {
return []; return [];
} }
if(this.hsrAnalyticUnit.detectorType === DetectorType.ANOMALY) { const hsrSerie = {
const confidence = (this.hsrAnalyticUnit as AnomalyAnalyticUnit).confidence; ...response.hsr,
color: ANALYTIC_UNIT_COLORS[0],
// TODO: render it separately from Metric series
overrides: [
{ alias: 'HSR', linewidth: 3, fill: 0 }
]
};
if(response.lowerBound !== undefined && response.upperBound !== undefined) {
// TODO: looks bad // TODO: looks bad
return [ return [
{ {
target: 'Confidence interval lower', target: '[AnomalyDetector]: lower bound',
datapoints: hsr.datapoints.map(datapoint => datapoints: response.lowerBound.datapoints,
[datapoint[0] - confidence, datapoint[1]]
),
color: ANALYTIC_UNIT_COLORS[0], color: ANALYTIC_UNIT_COLORS[0],
overrides: [{ alias: 'Confidence interval lower', linewidth: 1, fill: 0 }] overrides: [{ alias: '[AnomalyDetector]: lower bound', linewidth: 1, fill: 0 }]
}, },
{ {
target: 'Confidence interval upper', target: '[AnomalyDetector]: upper bound',
datapoints: hsr.datapoints.map(datapoint => datapoints: response.upperBound.datapoints,
[datapoint[0] + confidence, datapoint[1]]
),
color: ANALYTIC_UNIT_COLORS[0], color: ANALYTIC_UNIT_COLORS[0],
overrides: [{ alias: 'Confidence interval upper', linewidth: 1, fill: 0 }] overrides: [{ alias: '[AnomalyDetector]: upper bound', linewidth: 1, fill: 0 }]
}, },
hsrSerie
]; ];
} }
return { return hsrSerie;
...hsr,
color: ANALYTIC_UNIT_COLORS[0],
// TODO: render it separately from Metric series
overrides: [
{ alias: 'HSR', linewidth: 3, fill: 0 }
]
};
} }
get inspectedAnalyticUnit(): AnalyticUnit | null { get inspectedAnalyticUnit(): AnalyticUnit | null {
@ -562,16 +582,6 @@ export class AnalyticController {
return null; return null;
} }
get hsrAnalyticUnit(): AnalyticUnit | null {
// TODO: remove inspectedAnalyticUnit duplication
for(let analyticUnit of this.analyticUnits) {
if(analyticUnit.showHSR) {
return analyticUnit;
}
};
return null;
}
public get conditions() { public get conditions() {
return _.values(Condition); return _.values(Condition);
} }
@ -617,7 +627,7 @@ export class AnalyticController {
} }
analyticUnit.detectionSpans = data; analyticUnit.detectionSpans = data;
let isFinished = true; let isFinished = true;
for (let detection of data) { for(let detection of data) {
if(detection.status === DetectionStatus.RUNNING) { if(detection.status === DetectionStatus.RUNNING) {
isFinished = false; isFinished = false;
} }
@ -671,29 +681,28 @@ export class AnalyticController {
return this._tempIdCounted.toString(); return this._tempIdCounted.toString();
} }
public async toggleVisibility(id: AnalyticUnitId, value?: boolean) { public toggleVisibility(id: AnalyticUnitId, value?: boolean) {
const analyticUnit = this._analyticUnitsSet.byId(id); const analyticUnit = this._analyticUnitsSet.byId(id);
if(value !== undefined) { if(value !== undefined) {
analyticUnit.visible = value; analyticUnit.visible = value;
} else { } else {
analyticUnit.visible = !analyticUnit.visible; analyticUnit.visible = !analyticUnit.visible;
} }
await this.saveAnalyticUnit(analyticUnit); analyticUnit.changed = true;
} }
public toggleInspect(id: AnalyticUnitId) { public toggleInspect(id: AnalyticUnitId) {
const analyticUnit = this._analyticUnitsSet.byId(id); this.analyticUnits
if(!analyticUnit.inspect) { .filter(analyticUnit => analyticUnit.id !== id)
this.analyticUnits.forEach(unit => unit.inspect = false); .forEach(unit => unit.inspect = false);
}
} }
public toggleHSR(id: AnalyticUnitId) { public async updateSeasonality(id: AnalyticUnitId, value?: number) {
// TODO: remove toggleInspect duplication const analyticUnit = this._analyticUnitsSet.byId(id) as AnomalyAnalyticUnit;
const analyticUnit = this._analyticUnitsSet.byId(id); if(value !== undefined) {
if(!analyticUnit.showHSR) { analyticUnit.seasonalityPeriod.value = value;
this.analyticUnits.forEach(unit => unit.showHSR = false);
} }
analyticUnit.changed = true;
} }
public onAnalyticUnitDetectorChange(analyticUnitTypes: any) { public onAnalyticUnitDetectorChange(analyticUnitTypes: any) {
@ -730,3 +739,12 @@ function addAlphaToRGB(colorString: string, alpha: number): string {
return colorString; return colorString;
} }
} }
function convertTableToTimeSeries(target: string, tableData?: TableTimeSeries): HSRTimeSeries {
if(tableData === undefined) {
return undefined;
}
const datapoints = tableData.values.map(value => value.reverse() as [number, number]);
return { target, datapoints };
}

53
src/panel/graph_panel/graph_ctrl.ts

@ -272,12 +272,7 @@ class GraphCtrl extends MetricsPanelCtrl {
if(analyticUnit.status === '404') { if(analyticUnit.status === '404') {
await this.analyticsController.removeAnalyticUnit(analyticUnit.id, true); await this.analyticsController.removeAnalyticUnit(analyticUnit.id, true);
} }
if(analyticUnit.status === 'READY') { this.refresh();
const { from, to } = this.rangeTimestamp;
await this.analyticsController.fetchSegments(analyticUnit, from, to);
}
this.render(this.seriesList);
this.$scope.$digest();
}); });
appEvents.on('ds-request-response', data => { appEvents.on('ds-request-response', data => {
@ -326,7 +321,6 @@ class GraphCtrl extends MetricsPanelCtrl {
} }
this.analyticsController = new AnalyticController(this._grafanaUrl, this._panelId, this.panel, this.events, this.analyticService); this.analyticsController = new AnalyticController(this._grafanaUrl, this._panelId, this.panel, this.events, this.analyticService);
this.analyticsController.fetchAnalyticUnitsStatuses();
this._updatePanelInfo(); this._updatePanelInfo();
this.analyticsController.updateServerInfo(); this.analyticsController.updateServerInfo();
@ -398,23 +392,27 @@ class GraphCtrl extends MetricsPanelCtrl {
}; };
break; break;
} }
const from = _.find(series.datapoints, datapoint => datapoint[0] !== null);
const to = _.findLast(series.datapoints, datapoint => datapoint[0] !== null);
this._dataTimerange = {};
if(from !== undefined && to !== undefined) {
this._dataTimerange = { from: from[1], to: to[1] };
}
} }
} }
if(this.analyticsController !== undefined) { if(this.analyticsController !== undefined) {
await this.analyticsController.fetchAnalyticUnitsSegments(from, to);
// TODO: make statuses and detection spans connected
this.analyticsController.fetchAnalyticUnitsStatuses();
this.analyticsController.stopAnalyticUnitsDetectionsFetching(); this.analyticsController.stopAnalyticUnitsDetectionsFetching();
const loadTasks = [ // TODO: re-run detection waiters if this._dataTimerange is changed
// this.annotationsPromise,
this.analyticsController.fetchAnalyticUnitsSegments(from, to)
];
await Promise.all(loadTasks);
// this.annotations = results[0].annotations;
this.render(this.seriesList);
this.analyticsController.fetchAnalyticUnitsDetections( this.analyticsController.fetchAnalyticUnitsDetections(
this._dataTimerange.from, this._dataTimerange.from,
this._dataTimerange.to this._dataTimerange.to
); );
this.render(this.seriesList);
} }
this.loading = false; this.loading = false;
@ -426,14 +424,6 @@ class GraphCtrl extends MetricsPanelCtrl {
} }
for(let series of this.seriesList) { for(let series of this.seriesList) {
const from = _.find(series.datapoints, datapoint => datapoint[0] !== null);
const to = _.findLast(series.datapoints, datapoint => datapoint[0] !== null);
this._dataTimerange = {};
if(from !== undefined && to !== undefined) {
this._dataTimerange = { from: from[1], to: to[1] };
}
if (series.unit) { if (series.unit) {
this.panel.yaxes[series.yaxis - 1].format = series.unit; this.panel.yaxes[series.yaxis - 1].format = series.unit;
} }
@ -572,8 +562,13 @@ class GraphCtrl extends MetricsPanelCtrl {
this.analyticsController.createNew(); this.analyticsController.createNew();
} }
cancelCreation() {
this.analyticsController.cancelCreation();
}
redetectAll() { redetectAll() {
this.analyticsController.redetectAll(); const { from, to } = this.rangeTimestamp;
this.analyticsController.redetectAll(from, to);
} }
async runDetectInCurrentRange(analyticUnitId: AnalyticUnitId) { async runDetectInCurrentRange(analyticUnitId: AnalyticUnitId) {
@ -610,7 +605,11 @@ class GraphCtrl extends MetricsPanelCtrl {
await this.analyticsController.toggleAnalyticUnitAlert(analyticUnit); await this.analyticsController.toggleAnalyticUnitAlert(analyticUnit);
} }
async onAnalyticUnitChange(analyticUnit: AnalyticUnit) { onAnalyticUnitChange(analyticUnit: AnalyticUnit) {
this.analyticsController.toggleAnalyticUnitChange(analyticUnit, true);
}
async onAnalyticUnitSave(analyticUnit: AnalyticUnit) {
await this.analyticsController.saveAnalyticUnit(analyticUnit); await this.analyticsController.saveAnalyticUnit(analyticUnit);
this.refresh(); this.refresh();
} }
@ -683,8 +682,8 @@ class GraphCtrl extends MetricsPanelCtrl {
this.refresh(); this.refresh();
} }
onToggleHSR(id: AnalyticUnitId) { onSeasonalityChange(id: AnalyticUnitId, value?: number) {
this.analyticsController.toggleHSR(id); this.analyticsController.updateSeasonality(id, value);
this.refresh(); this.refresh();
} }

2
src/panel/graph_panel/graph_legend.ts

@ -46,7 +46,7 @@ export class GraphLegend {
position: 'bottom left', position: 'bottom left',
targetAttachment: 'top left', targetAttachment: 'top left',
template: template:
'<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected"/>', '<series-color-picker-popover series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected"/>',
openOn: 'hover', openOn: 'hover',
model: { model: {
series: series, series: series,

4
src/panel/graph_panel/graph_renderer.ts

@ -153,10 +153,10 @@ export class GraphRenderer {
this._analyticController.deleteLabelingAnalyticUnitSegmentsInRange( this._analyticController.deleteLabelingAnalyticUnitSegmentsInRange(
segment.from, segment.to segment.from, segment.to
); );
this._analyticController.addLabelSegment(segment, true); this._analyticController.addSegment(segment, true);
} }
if(this._analyticController.labelingMode === LabelingMode.LABELING) { if(this._analyticController.labelingMode === LabelingMode.LABELING) {
this._analyticController.addLabelSegment(segment, false); this._analyticController.addSegment(segment, false);
} }
if(this._analyticController.labelingMode === LabelingMode.UNLABELING) { if(this._analyticController.labelingMode === LabelingMode.UNLABELING) {
this._analyticController.deleteLabelingAnalyticUnitSegmentsInRange( this._analyticController.deleteLabelingAnalyticUnitSegmentsInRange(

27
src/panel/graph_panel/models/analytic_units/analytic_unit.ts

@ -33,6 +33,9 @@ export class AnalyticSegment extends Segment {
if(!_.isBoolean(this.labeled)) { if(!_.isBoolean(this.labeled)) {
throw new Error('labeled value is not boolean'); throw new Error('labeled value is not boolean');
} }
if(labeled && deleted) {
throw new Error('Segment can`t be both labeled and deleted');
}
} }
} }
@ -47,6 +50,8 @@ const DEFAULTS = {
visible: true visible: true
}; };
const LABELING_MODES = [];
export class AnalyticUnit { export class AnalyticUnit {
private _labelingMode: LabelingMode = LabelingMode.LABELING; private _labelingMode: LabelingMode = LabelingMode.LABELING;
@ -55,7 +60,7 @@ export class AnalyticUnit {
private _segmentSet = new SegmentArray<AnalyticSegment>(); private _segmentSet = new SegmentArray<AnalyticSegment>();
private _detectionSpans: DetectionSpan[]; private _detectionSpans: DetectionSpan[];
private _inspect = false; private _inspect = false;
private _showHSR = false; private _changed = false;
private _status: string; private _status: string;
private _error: string; private _error: string;
@ -113,12 +118,12 @@ export class AnalyticUnit {
get saving(): boolean { return this._saving; } get saving(): boolean { return this._saving; }
set saving(value: boolean) { this._saving = value; } set saving(value: boolean) { this._saving = value; }
get changed(): boolean { return this._changed; }
set changed(value: boolean) { this._changed = value; }
get inspect(): boolean { return this._inspect; } get inspect(): boolean { return this._inspect; }
set inspect(value: boolean) { this._inspect = value; } set inspect(value: boolean) { this._inspect = value; }
get showHSR(): boolean { return this._showHSR; }
set showHSR(value: boolean) { this._showHSR = value; }
get visible(): boolean { get visible(): boolean {
return (this._serverObject.visible === undefined) ? true : this._serverObject.visible return (this._serverObject.visible === undefined) ? true : this._serverObject.visible
} }
@ -126,10 +131,10 @@ export class AnalyticUnit {
this._serverObject.visible = value; this._serverObject.visible = value;
} }
addLabeledSegment(segment: Segment, deleted: boolean): AnalyticSegment { addSegment(segment: Segment, deleted: boolean): AnalyticSegment {
const asegment = new AnalyticSegment(!deleted, segment.id, segment.from, segment.to, deleted); const addedSegment = new AnalyticSegment(!deleted, segment.id, segment.from, segment.to, deleted);
this._segmentSet.addSegment(asegment); this._segmentSet.addSegment(addedSegment);
return asegment; return addedSegment;
} }
removeSegmentsInRange(from: number, to: number): AnalyticSegment[] { removeSegmentsInRange(from: number, to: number): AnalyticSegment[] {
@ -152,8 +157,10 @@ export class AnalyticUnit {
value !== '404' && value !== '404' &&
value !== 'READY' && value !== 'READY' &&
value !== 'LEARNING' && value !== 'LEARNING' &&
value !== 'DETECTION' &&
value !== 'PENDING' && value !== 'PENDING' &&
value !== 'FAILED' && value !== 'FAILED' &&
value !== 'SUCCESS' &&
value !== null value !== null
) { ) {
throw new Error('Unsupported status value: ' + value); throw new Error('Unsupported status value: ' + value);
@ -176,4 +183,8 @@ export class AnalyticUnit {
get serverObject() { return this._serverObject; } get serverObject() { return this._serverObject; }
// TODO: make it abstract
get labelingModes() {
return LABELING_MODES;
}
} }

49
src/panel/graph_panel/models/analytic_units/anomaly_analytic_unit.ts

@ -1,14 +1,30 @@
import { AnalyticUnit, DetectorType } from './analytic_unit'; import { AnalyticUnit, DetectorType, LabelingMode } from './analytic_unit';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment';
type TimePeriod = {
value: number,
unit: string
};
const DEFAULTS = { const DEFAULTS = {
detectorType: DetectorType.ANOMALY, detectorType: DetectorType.ANOMALY,
type: 'ANOMALY', type: 'ANOMALY',
alpha: 0.5, alpha: 0.5,
confidence: 1 confidence: 1,
seasonality: 0,
seasonalityPeriod: {
value: 0,
unit: 'seconds'
}
}; };
const LABELING_MODES = [
{ name: 'Label Negative', value: LabelingMode.DELETING },
{ name: 'Unlabel', value: LabelingMode.UNLABELING }
];
export class AnomalyAnalyticUnit extends AnalyticUnit { export class AnomalyAnalyticUnit extends AnalyticUnit {
constructor(_serverObject?: any) { constructor(_serverObject?: any) {
@ -21,7 +37,9 @@ export class AnomalyAnalyticUnit extends AnalyticUnit {
return { return {
...baseJSON, ...baseJSON,
alpha: this.alpha, alpha: this.alpha,
confidence: this.confidence confidence: this.confidence,
seasonality: this.seasonality,
seasonalityPeriod: this.seasonalityPeriod
}; };
} }
@ -30,4 +48,29 @@ export class AnomalyAnalyticUnit extends AnalyticUnit {
set confidence(val: number) { this._serverObject.confidence = val; } set confidence(val: number) { this._serverObject.confidence = val; }
get confidence(): number { return this._serverObject.confidence; } get confidence(): number { return this._serverObject.confidence; }
get seasonality(): number {
let seasonalityObj = {};
seasonalityObj[this.seasonalityPeriod.unit] = this.seasonalityPeriod.value;
return moment.duration(seasonalityObj).asMilliseconds();
}
set seasonalityPeriod(val: TimePeriod) { this._serverObject.seasonalityPeriod = val; }
get seasonalityPeriod(): TimePeriod { return this._serverObject.seasonalityPeriod; }
// TODO: merge seasonality and hasSeasonality
set hasSeasonality(val: boolean) {
if(val) {
this.seasonalityPeriod = { value: 1, unit: 'seconds' };
} else {
this.seasonalityPeriod = { value: 0, unit: 'seconds' };
}
}
get hasSeasonality(): boolean {
return this.seasonality > 0;
}
get labelingModes() {
return LABELING_MODES;
}
} }

12
src/panel/graph_panel/models/analytic_units/pattern_analytic_unit.ts

@ -1,4 +1,4 @@
import { AnalyticUnit, DetectorType } from './analytic_unit'; import { AnalyticUnit, DetectorType, LabelingMode } from './analytic_unit';
import _ from 'lodash'; import _ from 'lodash';
@ -7,6 +7,12 @@ const DEFAULTS = {
type: 'GENERAL' type: 'GENERAL'
}; };
const LABELING_MODES = [
{ name: 'Label Positive', value: LabelingMode.LABELING },
{ name: 'Label Negative', value: LabelingMode.DELETING },
{ name: 'Unlabel', value: LabelingMode.UNLABELING }
];
export class PatternAnalyticUnit extends AnalyticUnit { export class PatternAnalyticUnit extends AnalyticUnit {
constructor(_serverObject?: any) { constructor(_serverObject?: any) {
@ -20,4 +26,8 @@ export class PatternAnalyticUnit extends AnalyticUnit {
...baseJSON ...baseJSON
}; };
} }
get labelingModes() {
return LABELING_MODES;
}
} }

6
src/panel/graph_panel/models/analytic_units/threshold_analytic_unit.ts

@ -19,6 +19,8 @@ const DEFAULTS = {
condition: Condition.ABOVE_OR_EQUAL condition: Condition.ABOVE_OR_EQUAL
}; };
const LABELING_MODES = [];
export class ThresholdAnalyticUnit extends AnalyticUnit { export class ThresholdAnalyticUnit extends AnalyticUnit {
constructor(_serverObject?: any) { constructor(_serverObject?: any) {
@ -40,4 +42,8 @@ export class ThresholdAnalyticUnit extends AnalyticUnit {
set condition(val: Condition) { this._serverObject.condition = val; } set condition(val: Condition) { this._serverObject.condition = val; }
get condition(): Condition { return this._serverObject.condition; } get condition(): Condition { return this._serverObject.condition; }
get labelingModes() {
return LABELING_MODES;
}
} }

6
src/panel/graph_panel/models/detection.ts

@ -14,7 +14,7 @@ export type DetectionSpan = {
}; };
export const DETECTION_STATUS_TEXT = new Map<DetectionStatus, string>([ export const DETECTION_STATUS_TEXT = new Map<DetectionStatus, string>([
[DetectionStatus.READY, 'Detection is done'], [DetectionStatus.READY, '[DetectionStatus]: done'],
[DetectionStatus.RUNNING, 'Detection is running...'], [DetectionStatus.RUNNING, '[DetectionStatus]: running...'],
[DetectionStatus.FAILED, 'Detection failed'] [DetectionStatus.FAILED, '[DetectionStatus]: failed']
]); ]);

442
src/panel/graph_panel/partials/tab_analytics.html

@ -1,7 +1,7 @@
<div class="gf-form-group"> <div class="gf-form-group">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label">Select Hastic datasource</label> <label class="gf-form-label">Select Hastic datasource</label>
<select class="gf-form-input max-width-15" <select class="gf-form-input width-15"
ng-model="ctrl.panel.hasticDatasource" ng-model="ctrl.panel.hasticDatasource"
ng-options="ds.id as ds.name for ds in ctrl.hasticDatasources" ng-options="ds.id as ds.name for ds in ctrl.hasticDatasources"
ng-change="ctrl.onHasticDatasourceChange()" ng-change="ctrl.onHasticDatasourceChange()"
@ -12,7 +12,7 @@
<div class="gf-form"> <div class="gf-form">
<div class="gf-form-button-row" ng-if="ctrl.analyticsController.serverStatus === false"> <div class="gf-form-button-row" ng-if="ctrl.analyticsController.serverStatus === false">
<h5>Hastic server at "{{ctrl.hasticDatasource.url}}" is not available</h5> <h5>Hastic server at "{{ctrl.hasticDatasource.url}}" is not available</h5>
<button class="btn btn-inverse" ng-click="ctrl.runDatasourceConnectivityCheck()"> <button class="btn btn-inverse" ng-click="ctrl.onHasticDatasourceChange()">
<i class="fa fa-plug"></i> <i class="fa fa-plug"></i>
Reconnect to Hastic server Reconnect to Hastic server
</button> </button>
@ -20,213 +20,299 @@
<div ng-if="ctrl.analyticsController.serverStatus === true && !ctrl.analyticsController.loading"> <div ng-if="ctrl.analyticsController.serverStatus === true && !ctrl.analyticsController.loading">
<h5> Analytic Units </h5> <h5> Analytic Units </h5>
<div class="editor-row"> <div ng-repeat="analyticUnit in ctrl.analyticsController.analyticUnits">
<div class="gf-form" ng-repeat="analyticUnit in ctrl.analyticsController.analyticUnits"> <div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-5"> <label class="gf-form-label width-5">
<i class="fa fa-info" bs-tooltip="'Analytic unit id: ' + analyticUnit.id"></i> <i class="fa fa-info" bs-tooltip="'Analytic unit id: ' + analyticUnit.id"></i>
&nbsp; Name &nbsp; Name
</label> </label>
<input <input
type="text" class="gf-form-input max-width-15" type="text" class="gf-form-input width-15"
ng-model="analyticUnit.name" ng-model="analyticUnit.name"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)" ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
> >
<label class="gf-form-label width-4"> Type </label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input width-10"
ng-model="analyticUnit.type"
ng-options="type.value as type.name for type in ctrl.analyticUnitTypes[analyticUnit.detectorType]"
ng-disabled="true"
/>
</div> </div>
<!-- <div class="gf-form">
<label class="gf-form-label width-6"> Confidence </label> <label class="gf-form-label width-5"> Type </label>
<input <div class="gf-form-select-wrapper">
type="number" class="gf-form-input width-5 ng-valid ng-scope ng-empty ng-dirty ng-valid-number ng-touched" <select class="gf-form-input width-9"
placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" ng-model="analyticUnit.type"
data-placement="right" ng-model="ctrl.panel.decimals" ng-change="ctrl.render()" ng-model-onblur="" data-original-title="" title="" ng-options="type.value as type.name for type in ctrl.analyticUnitTypes[analyticUnit.detectorType]"
/> ng-disabled="true"
--> />
</div>
</div>
<label class="gf-form-label width-8"> Positive Color </label> <div class="gf-form">
<span class="gf-form-label"> <label class="gf-form-label width-8"> Positive Color </label>
<color-picker <span class="gf-form-label">
color="analyticUnit.labeledColor" <color-picker
onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id, false)" color="analyticUnit.labeledColor"
/> onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id, false)"
</span> />
</span>
</div>
<!-- Hack to avoid Grafana's .gf-form-label + .gf-form-label css. Fix for https://github.com/hastic/hastic-grafana-app/issues/192 --> <div class="gf-form"
<div ng-if="analyticUnit.detectorType === 'pattern'"></div> ng-if="analyticUnit.detectorType === 'pattern' || analyticUnit.detectorType === 'anomaly'"
>
<label class="gf-form-label width-8"> Negative Color </label>
<span class="gf-form-label">
<color-picker
color="analyticUnit.deletedColor"
onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id, true)"
/>
</span>
</div>
<label ng-if="analyticUnit.detectorType === 'pattern'" class="gf-form-label width-8"> Negative Color </label> <div class="gf-form" ng-if="analyticUnit.visible">
<span ng-if="analyticUnit.detectorType === 'pattern'" class="gf-form-label"> <!-- TODO: Remove hack with "margin-bottom: 0" -->
<color-picker <gf-form-switch
color="analyticUnit.deletedColor" class="gf-form"
onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id, true)" style="margin-bottom: 0"
label="Inspect"
label-class="width-5"
on-change="ctrl.onToggleInspect(analyticUnit.id)"
checked="analyticUnit.inspect"
/> />
</span> </div>
<!-- TODO: move analytic-unit-specific fields rendering to class --> <div
<select class="gf-form-input width-9" class="gf-form"
ng-if="analyticUnit.detectorType === 'threshold'"
ng-model="analyticUnit.condition"
ng-options="type for type in ctrl.analyticsController.conditions"
ng-change="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
<input class="gf-form-input width-5"
ng-if=" ng-if="
analyticUnit.detectorType === 'threshold' && analyticUnit.detectorType === 'pattern' ||
analyticUnit.condition !== 'NO_DATA' (analyticUnit.detectorType === 'anomaly' && analyticUnit.hasSeasonality)
" "
type="number" >
ng-model="analyticUnit.value" <label
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)" class="gf-form-label pointer"
/> ng-if="!analyticUnit.selected"
ng-style="analyticUnit.status === 'LEARNING' && { 'cursor': 'not-allowed' }"
ng-click="ctrl.onToggleLabelingMode(analyticUnit.id)"
ng-disabled="analyticUnit.status === 'LEARNING'"
>
<i class="fa fa-bar-chart" ng-if="!analyticUnit.saving"></i>
<i class="fa fa-spinner fa-spin" ng-if="analyticUnit.saving"></i>
Label
</label>
<select class="gf-form-input width-12"
ng-if="analyticUnit.selected && !analyticUnit.saving"
ng-model="ctrl.analyticsController.labelingMode"
ng-options="type.value as type.name for type in analyticUnit.labelingModes"
ng-disabled="analyticUnit.status === 'LEARNING'"
/>
</div>
<label class="gf-form-label width-6" ng-if="analyticUnit.detectorType === 'anomaly'"> Alpha </label> <div class="gf-form" ng-if="!analyticUnit.selected">
<input class="gf-form-input width-5" <label class="gf-form-label">
ng-if="analyticUnit.detectorType === 'anomaly'" <a
min="0" ng-if="analyticUnit.visible"
max="1" bs-tooltip="'Hide. It`s visible now.'"
type="number" ng-click="ctrl.onToggleVisibility(analyticUnit.id)"
ng-model="analyticUnit.alpha" class="pointer"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)" >
/> <i class="fa fa-eye"></i>
</a>
<label class="gf-form-label width-6" ng-if="analyticUnit.detectorType === 'anomaly'"> Confidence </label> <a
<input class="gf-form-input width-5" ng-if="!analyticUnit.visible"
ng-if="analyticUnit.detectorType === 'anomaly'" bs-tooltip="'Show. It`s hidden now.'"
min="0" ng-click="ctrl.onToggleVisibility(analyticUnit.id)"
type="number" class="pointer"
ng-model="analyticUnit.confidence" >
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)" <i class="fa fa-eye-slash"></i>
/> </a>
</label>
</div>
<label class="gf-form-label" ng-if="analyticUnit.status === 'READY' && analyticUnit.visible"> <div class="gf-form" ng-if="!analyticUnit.selected">
Inspect <a
</label> class="btn btn-danger"
ng-click="ctrl.onRemove(analyticUnit.id)"
>
<i class="fa fa-trash"></i>
</a>
</div>
<gf-form-switch ng-if="analyticUnit.status === 'READY' && analyticUnit.visible" <div class="gf-form" ng-if="analyticUnit.selected">
on-change="ctrl.onToggleInspect(analyticUnit.id)" <div class="gf-form-label">
checked="analyticUnit.inspect" <a
/> class="pointer"
ng-click="ctrl.onCancelLabeling(analyticUnit.id)"
>
Cancel
</a>
</div>
</div>
<label class="gf-form-label" ng-if="analyticUnit.visible"> <div class="gf-form">
HSR <label>
</label> <i ng-if="analyticUnit.status === 'READY'" class="grafana-tip fa fa-check-circle ng-scope" bs-tooltip="'Ready'"></i>
<i ng-if="analyticUnit.status === 'SUCCESS'" class="grafana-tip fa fa-check ng-scope" bs-tooltip="'Learning succeeded'"></i>
<i ng-if="analyticUnit.status === 'LEARNING'" class="grafana-tip fa fa-leanpub ng-scope" bs-tooltip="'Learning'"></i>
<i ng-if="analyticUnit.status === 'DETECTION'" class="grafana-tip fa fa-search ng-scope" bs-tooltip="'Detection'"></i>
<i ng-if="analyticUnit.status === 'PENDING'" class="grafana-tip fa fa-list-ul ng-scope" bs-tooltip="'Pending'"></i>
<i ng-if="analyticUnit.status === 'FAILED'" class="grafana-tip fa fa-exclamation-circle ng-scope" bs-tooltip="'Error: ' + analyticUnit.error"></i>
</label>
</div>
</div>
<gf-form-switch ng-if="analyticUnit.visible" <div class="gf-form-inline">
on-change="ctrl.onToggleHSR(analyticUnit.id)" <div class="gf-form width-20"/>
checked="analyticUnit.showHSR" <!-- TODO: move analytic-unit-specific fields rendering to class -->
/> <div class="gf-form" ng-if="analyticUnit.detectorType === 'threshold'">
<label class="gf-form-label width-5"> Condition </label>
<select class="gf-form-input"
ng-class="{
'width-5': analyticUnit.condition !== 'NO_DATA',
'width-9': analyticUnit.condition === 'NO_DATA'
}"
ng-model="analyticUnit.condition"
ng-options="type for type in ctrl.analyticsController.conditions"
ng-change="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
<input class="gf-form-input width-4"
ng-if="analyticUnit.condition !== 'NO_DATA'"
type="number"
ng-model="analyticUnit.value"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
</div>
<label class="gf-form-label" ng-hide="analyticUnit.selected"> <div class="gf-form" ng-if="analyticUnit.detectorType === 'anomaly'">
<a <label class="gf-form-label width-5"> Alpha </label>
ng-if="analyticUnit.visible" <input class="gf-form-input width-9"
bs-tooltip="'Hide. It`s visible now.'" min="0"
ng-click="ctrl.onToggleVisibility(analyticUnit.id)" max="1"
class="pointer" type="number"
> ng-model="analyticUnit.alpha"
<i class="fa fa-eye"></i> ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
</a> />
</div>
<a <div class="gf-form" ng-if="analyticUnit.detectorType === 'anomaly'">
ng-if="!analyticUnit.visible" <label class="gf-form-label width-6"> Confidence </label>
bs-tooltip="'Show. It`s hidden now.'" <input class="gf-form-input width-5"
ng-click="ctrl.onToggleVisibility(analyticUnit.id)" min="0"
class="pointer" type="number"
> ng-model="analyticUnit.confidence"
<i class="fa fa-eye-slash"></i> ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
</a> />
</label> </div>
<div class="gf-form" ng-if="analyticUnit.detectorType === 'anomaly'">
<!-- TODO: Remove hack with "margin-bottom: 0" -->
<gf-form-switch
class="gf-form"
style="margin-bottom: 0"
label="Seasonality"
label-class="width-7"
on-change="ctrl.onAnalyticUnitChange(analyticUnit)"
checked="analyticUnit.hasSeasonality"
/>
</div>
<label class="gf-form-label" <div
ng-if="analyticUnit.detectorType === 'threshold' || analyticUnit.detectorType === 'anomaly'" class="gf-form"
ng-if="analyticUnit.detectorType === 'anomaly' && analyticUnit.hasSeasonality"
> >
<a class="pointer" ng-click="ctrl.runDetect(analyticUnit.id)"> <label class="gf-form-label width-9"> Seasonality Period </label>
<i class="fa fa-spinner fa-spin" ng-if="analyticUnit.saving"></i> <input
<b ng-if="!analyticUnit.saving"> Detect </b> type="number" class="gf-form-input width-5"
<b ng-if="analyticUnit.saving" ng-disabled="true"> saving... </b> ng-init="seasonalityValue = analyticUnit.seasonalityPeriod.value"
</a> ng-model="seasonalityValue"
</label> ng-blur="ctrl.onSeasonalityChange(analyticUnit.id, seasonalityValue)"
min="0"
>
</div>
<label <div class="gf-form"
class="gf-form-label" ng-if="
ng-if="analyticUnit.detectorType === 'pattern'" analyticUnit.detectorType === 'anomaly' &&
ng-style="analyticUnit.status === 'LEARNING' && { 'cursor': 'not-allowed' }" analyticUnit.hasSeasonality
"
> >
<a class="pointer" tabindex="1" <div class="gf-form-select-wrapper">
ng-click="ctrl.onToggleLabelingMode(analyticUnit.id)" <!-- TODO: move periods from ng-options -->
ng-disabled="analyticUnit.status === 'LEARNING'" <select class="gf-form-input width-8"
ng-model="analyticUnit.seasonalityPeriod.unit"
ng-change="ctrl.onSeasonalityChange(analyticUnit.id)"
ng-options="type for type in ['seconds', 'minutes', 'hours', 'days', 'years']"
/>
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form width-20"/>
<div class="gf-form">
<button class="btn btn-secondary"
ng-click="ctrl.onAnalyticUnitSave(analyticUnit)"
ng-disabled="!analyticUnit.changed"
> >
<i class="fa fa-bar-chart" ng-if="!analyticUnit.saving"></i>
<i class="fa fa-spinner fa-spin" ng-if="analyticUnit.saving"></i> <i class="fa fa-spinner fa-spin" ng-if="analyticUnit.saving"></i>
<b ng-if="!analyticUnit.saving"> Save </b>
<b ng-if="analyticUnit.saving" ng-disabled="true"> saving... </b> <b ng-if="analyticUnit.saving" ng-disabled="true"> saving... </b>
</a> </button>
</label> </div>
<select class="gf-form-input width-12"
ng-model="ctrl.analyticsController.labelingMode"
ng-options="type.value as type.name for type in [
{ name:'Label Positive', value: 'LABELING' },
{ name:'Label Negative', value: 'DELETING' },
{ name:'Unlabel', value: 'UNLABELING' }
]"
ng-if="analyticUnit.selected && !analyticUnit.saving"
ng-disabled="analyticUnit.status === 'LEARNING'"
/>
<label class="gf-form-label"> <!-- TODO: Leave one Detect button instead of 2 -->
<a <div class="gf-form"
ng-if="!analyticUnit.selected" ng-if="
ng-click="ctrl.onRemove(analyticUnit.id)" analyticUnit.detectorType === 'pattern' ||
class="pointer" (analyticUnit.detectorType === 'anomaly' && analyticUnit.hasSeasonality)
"
>
<button class="btn btn-secondary"
ng-click="ctrl.onToggleLabelingMode(analyticUnit.id)"
ng-disabled="analyticUnit.status === 'LEARNING' || analyticUnit.saving || analyticUnit.changed || !analyticUnit.selected"
> >
<i class="fa fa-trash"></i> <b> Detect </b>
</a> </button>
</div>
<a <div class="gf-form"
ng-if="analyticUnit.selected" ng-if="
ng-click="ctrl.onCancelLabeling(analyticUnit.id)" analyticUnit.detectorType === 'threshold' ||
class="pointer" (analyticUnit.detectorType === 'anomaly' && !analyticUnit.hasSeasonality)
"
>
<button class="btn btn-secondary"
ng-click="ctrl.runDetectInCurrentRange(analyticUnit.id)"
ng-disabled="analyticUnit.status === 'LEARNING' || analyticUnit.saving || analyticUnit.changed"
> >
<i class="fa fa-ban"></i> <b> Detect </b>
</a> </button>
</label> </div>
<label>
<i ng-if="analyticUnit.status === 'READY'" class="grafana-tip fa fa-check-circle ng-scope" bs-tooltip="'Ready'"></i>
<i ng-if="analyticUnit.status === 'LEARNING'" class="grafana-tip fa fa-leanpub ng-scope" bs-tooltip="'Learning'"></i>
<i ng-if="analyticUnit.status === 'PENDING'" class="grafana-tip fa fa-list-ul ng-scope" bs-tooltip="'Pending'"></i>
<i ng-if="analyticUnit.status === 'FAILED'" class="grafana-tip fa fa-exclamation-circle ng-scope" bs-tooltip="'Error: ' + analyticUnit.error"></i>
</label>
</div> </div>
</div> </div>
<div class="editor-row" ng-if="ctrl.analyticsController.creatingNew"> <div class="gf-form-inline" ng-if="ctrl.analyticsController.creatingNew">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-4"> Name </label> <label class="gf-form-label width-5"> Name </label>
<input <input
type="text" class="gf-form-input max-width-15" type="text" class="gf-form-input width-15"
ng-model="ctrl.analyticsController.newAnalyticUnit.name" ng-model="ctrl.analyticsController.newAnalyticUnit.name"
> >
</div>
<div class="gf-form">
<label class="gf-form-label width-8"> Detector Type </label> <label class="gf-form-label width-8"> Detector Type </label>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="gf-form-input width-10" <select class="gf-form-input width-8"
ng-model="ctrl.analyticsController.newAnalyticUnit.detectorType" ng-model="ctrl.analyticsController.newAnalyticUnit.detectorType"
ng-options="analyticUnitDetectorType for analyticUnitDetectorType in ctrl.analyticUnitDetectorTypes" ng-options="analyticUnitDetectorType for analyticUnitDetectorType in ctrl.analyticUnitDetectorTypes"
ng-change="ctrl.analyticsController.onAnalyticUnitDetectorChange(ctrl.analyticUnitTypes);" ng-change="ctrl.analyticsController.onAnalyticUnitDetectorChange(ctrl.analyticUnitTypes);"
/> />
</div> </div>
</div>
<label class="gf-form-label width-8"> Type </label> <div class="gf-form">
<label class="gf-form-label width-5"> Type </label>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="gf-form-input width-10" <select class="gf-form-input width-9"
ng-model="ctrl.analyticsController.newAnalyticUnit.type" ng-model="ctrl.analyticsController.newAnalyticUnit.type"
ng-options=" ng-options="
type.value as type.name type.value as type.name
@ -234,23 +320,33 @@
" "
/> />
</div> </div>
</div>
<label class="gf-form-label"> <div class="gf-form">
<a class="pointer" tabindex="1" ng-click="ctrl.saveNew()"> <a class="btn btn-danger" ng-click="ctrl.cancelCreation()">
<b ng-if="!ctrl.analyticsController.saving"> create </b> <b> Cancel </b>
<b ng-if="ctrl.analyticsController.saving" ng-disabled="true"> saving... </b> </a>
</a> </div>
</label>
<div class="gf-form">
<a class="btn btn-secondary" ng-click="ctrl.saveNew()">
<b ng-if="!ctrl.analyticsController.saving"> Create </b>
<b ng-if="ctrl.analyticsController.saving" ng-disabled="true"> Saving... </b>
</a>
</div> </div>
</div> </div>
<div class="gf-form-button-row" ng-if="!ctrl.analyticsController.creatingAnalyticUnit"> <div class="gf-form-button-row">
<button class="btn btn-inverse width-12" ng-click="ctrl.createNew()"> <button
class="btn btn-secondary width-12"
ng-click="ctrl.createNew()"
ng-disabled="ctrl.analyticsController.creatingNew"
>
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
Add Analytic Unit Add Analytic Unit
</button> </button>
</div> </div>
<div class="gf-form-button-row"> <div class="gf-form-button-row" ng-if="ctrl.analyticsController.analyticUnits.length > 0">
<button class="gf-form-label width-12 pointer" ng-click="ctrl.redetectAll()"> <button class="gf-form-label width-12 pointer" ng-click="ctrl.redetectAll()">
Re-detect all analytic units Re-detect all analytic units
</button> </button>

2
src/panel/graph_panel/series_overrides_ctrl.ts

@ -54,7 +54,7 @@ export class SeriesOverridesCtrl {
element: $element.find('.dropdown')[0], element: $element.find('.dropdown')[0],
position: 'top center', position: 'top center',
openOn: 'click', openOn: 'click',
template: '<series-color-picker series="series" onColorChange="colorSelected" />', template: '<series-color-picker-popover series="series" onColorChange="colorSelected" />',
model: { model: {
autoClose: true, autoClose: true,
colorSelected: $scope.colorSelected, colorSelected: $scope.colorSelected,

17
src/panel/graph_panel/services/analytic_service.ts

@ -12,6 +12,11 @@ import { appEvents } from 'grafana/app/core/core';
import * as _ from 'lodash'; import * as _ from 'lodash';
// TODO: TableTimeSeries is bad name
export type TableTimeSeries = {
values: [number, number][];
columns: string[];
};
export class AnalyticService { export class AnalyticService {
private _isUp: boolean = false; private _isUp: boolean = false;
@ -201,10 +206,14 @@ export class AnalyticService {
} }
async getHSR(analyticUnitId: AnalyticUnitId, from: number, to: number): Promise<{ async getHSR(analyticUnitId: AnalyticUnitId, from: number, to: number): Promise<{
values: [number, number][]; hsr: TableTimeSeries,
columns: string[]; lowerBound?: TableTimeSeries,
}> { upperBound?: TableTimeSeries
} | null> {
const data = await this.get('/query', { analyticUnitId, from, to }); const data = await this.get('/query', { analyticUnitId, from, to });
if(data === undefined) {
return null;
}
return data.results; return data.results;
} }
@ -242,7 +251,7 @@ export class AnalyticService {
} catch(error) { } catch(error) {
// xhrStatus may be one of: ('complete', 'error', 'timeout' or 'abort') // xhrStatus may be one of: ('complete', 'error', 'timeout' or 'abort')
// See: https://github.com/angular/angular.js/blob/55075b840c9194b8524627a293d6166528b9a1c2/src/ng/http.js#L919-L920 // See: https://github.com/angular/angular.js/blob/55075b840c9194b8524627a293d6166528b9a1c2/src/ng/http.js#L919-L920
if(error.xhrStatus !== 'complete') { if(error.xhrStatus !== 'complete' || error.status === 502) {
this.displayConnectionErrorAlert(); this.displayConnectionErrorAlert();
this._isUp = false; this._isUp = false;
} else { } else {

2
src/plugin.json

@ -11,7 +11,7 @@
"small": "img/icn-graph-panel.png", "small": "img/icn-graph-panel.png",
"large": "img/icn-graph-panel.png" "large": "img/icn-graph-panel.png"
}, },
"version": "0.3.3" "version": "0.3.4"
}, },
"includes": [ "includes": [
{ "type": "panel", "name": "Hastic Graph Panel" }, { "type": "panel", "name": "Hastic Graph Panel" },

2
src/utlis.ts

@ -1,7 +1,7 @@
import url from 'url-parse'; import url from 'url-parse';
import * as _ from 'lodash'; import * as _ from 'lodash';
export const SUPPORTED_SERVER_VERSION = '0.3.3-beta'; export const SUPPORTED_SERVER_VERSION = '0.3.4-beta';
export function normalizeUrl(inputUrl: string) { export function normalizeUrl(inputUrl: string) {
if(!inputUrl) { if(!inputUrl) {

Loading…
Cancel
Save