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)
* [FAQ](https://github.com/hastic/hastic-grafana-app/wiki/FAQ)
* [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)
# Prerequisites
* [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

2
package.json

@ -1,6 +1,6 @@
{
"name": "grafana-hastic-app",
"version": "0.3.3",
"version": "0.3.4",
"description": "Hastic app: labeling and rendeting analytics from hastic-server",
"main": "dist/module",
"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
import { AnalyticService } from '../services/analytic_service';
import { AnalyticService, TableTimeSeries } from '../services/analytic_service';
import {
AnalyticUnitId, AnalyticUnit,
AnalyticSegment, AnalyticSegmentsSearcher, AnalyticSegmentPair,
LabelingMode,
DetectorType
LabelingMode
} from '../models/analytic_units/analytic_unit';
import { AnomalyAnalyticUnit } from '../models/analytic_units/anomaly_analytic_unit';
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) {
this._savingNewAnalyticUnit = true;
const newAnalyticUnit = createAnalyticUnit(this._newAnalyticUnit.serverObject);
const newAnalyticUnit = createAnalyticUnit(this._newAnalyticUnit.toJSON());
newAnalyticUnit.id = await this._analyticService.postNewAnalyticUnit(
newAnalyticUnit, metric, datasource, this._grafanaUrl, this._panelId
);
@ -143,7 +147,8 @@ export class AnalyticController {
await this.disableLabeling();
this._selectedAnalyticUnitId = id;
this.labelingUnit.selected = true;
this.toggleLabelingMode(LabelingMode.LABELING);
const labelingModes = this.labelingUnit.labelingModes;
this.toggleLabelingMode(labelingModes[0].value);
}
async disableLabeling() {
@ -196,9 +201,9 @@ export class AnalyticController {
this.labelingUnit.labelingMode = labelingMode;
}
addLabelSegment(segment: Segment, deleted = false) {
const asegment = this.labelingUnit.addLabeledSegment(segment, deleted);
this._labelingDataAddedSegments.addSegment(asegment);
addSegment(segment: Segment, deleted = false) {
const addedSegment = this.labelingUnit.addSegment(segment, deleted);
this._labelingDataAddedSegments.addSegment(addedSegment);
}
get analyticUnits(): AnalyticUnit[] {
@ -215,7 +220,7 @@ export class AnalyticController {
} else {
analyticUnit.labeledColor = value;
}
await this.saveAnalyticUnit(analyticUnit);
analyticUnit.changed = true;
}
fetchAnalyticUnitsStatuses() {
@ -309,7 +314,7 @@ export class AnalyticController {
return newIds;
}
async redetectAll() {
async redetectAll(from?: number, to?: number) {
this.analyticUnits.forEach(unit => {
// TODO: remove duplication with runDetect
unit.segments.clear();
@ -317,9 +322,9 @@ export class AnalyticController {
unit.status = null;
});
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) {
@ -453,11 +458,7 @@ export class AnalyticController {
if(!this.inLabelingMode) {
throw new Error(`Can't enter ${labelingMode} mode when labeling mode is disabled`);
}
if(this.labelingUnit.labelingMode === labelingMode) {
this.labelingUnit.labelingMode = LabelingMode.LABELING;
} else {
this.labelingUnit.labelingMode = labelingMode;
}
this.labelingUnit.labelingMode = labelingMode;
}
async removeAnalyticUnit(id: AnalyticUnitId, silent: boolean = false): Promise<void> {
@ -478,14 +479,19 @@ export class AnalyticController {
await this._analyticService.setAnalyticUnitAlert(analyticUnit);
}
toggleAnalyticUnitChange(analyticUnit: AnalyticUnit, value: boolean): void {
analyticUnit.changed = value;
}
async saveAnalyticUnit(analyticUnit: AnalyticUnit): Promise<void> {
if(analyticUnit.id === null || analyticUnit.id === undefined) {
throw new Error('Cannot save analytic unit without id');
}
analyticUnit.saving = true;
await this._analyticService.updateAnalyticUnit(analyticUnit.serverObject);
await this._analyticService.updateAnalyticUnit(analyticUnit.toJSON());
analyticUnit.saving = false;
analyticUnit.changed = false;
}
async getAnalyticUnits(): Promise<any[]> {
@ -503,54 +509,68 @@ export class AnalyticController {
this.fetchAnalyticUnitsStatuses();
}
async getHSR(from: number, to: number): Promise<HSRTimeSeries | null> {
// Returns HSR (Hastic Signal Representation) for analytic unit with enabled "Show HSR"
// Returns null when there is no analytic units which have "Show HSR" enabled
if(this.hsrAnalyticUnit === null) {
async getHSR(from: number, to: number): Promise<{
hsr: HSRTimeSeries,
lowerBound?: HSRTimeSeries,
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;
}
const hsr = await this._analyticService.getHSR(this.hsrAnalyticUnit.id, from, to);
const datapoints = hsr.values.map(value => value.reverse() as [number, number]);
return { target: 'HSR', datapoints };
const response = await this._analyticService.getHSR(this.inspectedAnalyticUnit.id, from, to);
if(response === null) {
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) {
const hsr = await this.getHSR(from, to);
const response = await this.getHSR(from, to);
if(hsr === null) {
if(response === null) {
return [];
}
if(this.hsrAnalyticUnit.detectorType === DetectorType.ANOMALY) {
const confidence = (this.hsrAnalyticUnit as AnomalyAnalyticUnit).confidence;
const hsrSerie = {
...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
return [
{
target: 'Confidence interval lower',
datapoints: hsr.datapoints.map(datapoint =>
[datapoint[0] - confidence, datapoint[1]]
),
target: '[AnomalyDetector]: lower bound',
datapoints: response.lowerBound.datapoints,
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',
datapoints: hsr.datapoints.map(datapoint =>
[datapoint[0] + confidence, datapoint[1]]
),
target: '[AnomalyDetector]: upper bound',
datapoints: response.upperBound.datapoints,
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 {
...hsr,
color: ANALYTIC_UNIT_COLORS[0],
// TODO: render it separately from Metric series
overrides: [
{ alias: 'HSR', linewidth: 3, fill: 0 }
]
};
return hsrSerie;
}
get inspectedAnalyticUnit(): AnalyticUnit | null {
@ -562,16 +582,6 @@ export class AnalyticController {
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() {
return _.values(Condition);
}
@ -617,7 +627,7 @@ export class AnalyticController {
}
analyticUnit.detectionSpans = data;
let isFinished = true;
for (let detection of data) {
for(let detection of data) {
if(detection.status === DetectionStatus.RUNNING) {
isFinished = false;
}
@ -671,29 +681,28 @@ export class AnalyticController {
return this._tempIdCounted.toString();
}
public async toggleVisibility(id: AnalyticUnitId, value?: boolean) {
public toggleVisibility(id: AnalyticUnitId, value?: boolean) {
const analyticUnit = this._analyticUnitsSet.byId(id);
if(value !== undefined) {
analyticUnit.visible = value;
} else {
analyticUnit.visible = !analyticUnit.visible;
}
await this.saveAnalyticUnit(analyticUnit);
analyticUnit.changed = true;
}
public toggleInspect(id: AnalyticUnitId) {
const analyticUnit = this._analyticUnitsSet.byId(id);
if(!analyticUnit.inspect) {
this.analyticUnits.forEach(unit => unit.inspect = false);
}
this.analyticUnits
.filter(analyticUnit => analyticUnit.id !== id)
.forEach(unit => unit.inspect = false);
}
public toggleHSR(id: AnalyticUnitId) {
// TODO: remove toggleInspect duplication
const analyticUnit = this._analyticUnitsSet.byId(id);
if(!analyticUnit.showHSR) {
this.analyticUnits.forEach(unit => unit.showHSR = false);
public async updateSeasonality(id: AnalyticUnitId, value?: number) {
const analyticUnit = this._analyticUnitsSet.byId(id) as AnomalyAnalyticUnit;
if(value !== undefined) {
analyticUnit.seasonalityPeriod.value = value;
}
analyticUnit.changed = true;
}
public onAnalyticUnitDetectorChange(analyticUnitTypes: any) {
@ -730,3 +739,12 @@ function addAlphaToRGB(colorString: string, alpha: number): string {
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') {
await this.analyticsController.removeAnalyticUnit(analyticUnit.id, true);
}
if(analyticUnit.status === 'READY') {
const { from, to } = this.rangeTimestamp;
await this.analyticsController.fetchSegments(analyticUnit, from, to);
}
this.render(this.seriesList);
this.$scope.$digest();
this.refresh();
});
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.fetchAnalyticUnitsStatuses();
this._updatePanelInfo();
this.analyticsController.updateServerInfo();
@ -398,23 +392,27 @@ class GraphCtrl extends MetricsPanelCtrl {
};
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) {
await this.analyticsController.fetchAnalyticUnitsSegments(from, to);
// TODO: make statuses and detection spans connected
this.analyticsController.fetchAnalyticUnitsStatuses();
this.analyticsController.stopAnalyticUnitsDetectionsFetching();
const loadTasks = [
// this.annotationsPromise,
this.analyticsController.fetchAnalyticUnitsSegments(from, to)
];
await Promise.all(loadTasks);
// this.annotations = results[0].annotations;
this.render(this.seriesList);
// TODO: re-run detection waiters if this._dataTimerange is changed
this.analyticsController.fetchAnalyticUnitsDetections(
this._dataTimerange.from,
this._dataTimerange.to
);
this.render(this.seriesList);
}
this.loading = false;
@ -426,14 +424,6 @@ class GraphCtrl extends MetricsPanelCtrl {
}
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) {
this.panel.yaxes[series.yaxis - 1].format = series.unit;
}
@ -572,8 +562,13 @@ class GraphCtrl extends MetricsPanelCtrl {
this.analyticsController.createNew();
}
cancelCreation() {
this.analyticsController.cancelCreation();
}
redetectAll() {
this.analyticsController.redetectAll();
const { from, to } = this.rangeTimestamp;
this.analyticsController.redetectAll(from, to);
}
async runDetectInCurrentRange(analyticUnitId: AnalyticUnitId) {
@ -610,7 +605,11 @@ class GraphCtrl extends MetricsPanelCtrl {
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);
this.refresh();
}
@ -683,8 +682,8 @@ class GraphCtrl extends MetricsPanelCtrl {
this.refresh();
}
onToggleHSR(id: AnalyticUnitId) {
this.analyticsController.toggleHSR(id);
onSeasonalityChange(id: AnalyticUnitId, value?: number) {
this.analyticsController.updateSeasonality(id, value);
this.refresh();
}

2
src/panel/graph_panel/graph_legend.ts

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

4
src/panel/graph_panel/graph_renderer.ts

@ -153,10 +153,10 @@ export class GraphRenderer {
this._analyticController.deleteLabelingAnalyticUnitSegmentsInRange(
segment.from, segment.to
);
this._analyticController.addLabelSegment(segment, true);
this._analyticController.addSegment(segment, true);
}
if(this._analyticController.labelingMode === LabelingMode.LABELING) {
this._analyticController.addLabelSegment(segment, false);
this._analyticController.addSegment(segment, false);
}
if(this._analyticController.labelingMode === LabelingMode.UNLABELING) {
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)) {
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
};
const LABELING_MODES = [];
export class AnalyticUnit {
private _labelingMode: LabelingMode = LabelingMode.LABELING;
@ -55,7 +60,7 @@ export class AnalyticUnit {
private _segmentSet = new SegmentArray<AnalyticSegment>();
private _detectionSpans: DetectionSpan[];
private _inspect = false;
private _showHSR = false;
private _changed = false;
private _status: string;
private _error: string;
@ -113,12 +118,12 @@ export class AnalyticUnit {
get saving(): boolean { return this._saving; }
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; }
set inspect(value: boolean) { this._inspect = value; }
get showHSR(): boolean { return this._showHSR; }
set showHSR(value: boolean) { this._showHSR = value; }
get visible(): boolean {
return (this._serverObject.visible === undefined) ? true : this._serverObject.visible
}
@ -126,10 +131,10 @@ export class AnalyticUnit {
this._serverObject.visible = value;
}
addLabeledSegment(segment: Segment, deleted: boolean): AnalyticSegment {
const asegment = new AnalyticSegment(!deleted, segment.id, segment.from, segment.to, deleted);
this._segmentSet.addSegment(asegment);
return asegment;
addSegment(segment: Segment, deleted: boolean): AnalyticSegment {
const addedSegment = new AnalyticSegment(!deleted, segment.id, segment.from, segment.to, deleted);
this._segmentSet.addSegment(addedSegment);
return addedSegment;
}
removeSegmentsInRange(from: number, to: number): AnalyticSegment[] {
@ -152,8 +157,10 @@ export class AnalyticUnit {
value !== '404' &&
value !== 'READY' &&
value !== 'LEARNING' &&
value !== 'DETECTION' &&
value !== 'PENDING' &&
value !== 'FAILED' &&
value !== 'SUCCESS' &&
value !== null
) {
throw new Error('Unsupported status value: ' + value);
@ -176,4 +183,8 @@ export class AnalyticUnit {
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 moment from 'moment';
type TimePeriod = {
value: number,
unit: string
};
const DEFAULTS = {
detectorType: DetectorType.ANOMALY,
type: 'ANOMALY',
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 {
constructor(_serverObject?: any) {
@ -21,7 +37,9 @@ export class AnomalyAnalyticUnit extends AnalyticUnit {
return {
...baseJSON,
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; }
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';
@ -7,6 +7,12 @@ const DEFAULTS = {
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 {
constructor(_serverObject?: any) {
@ -20,4 +26,8 @@ export class PatternAnalyticUnit extends AnalyticUnit {
...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
};
const LABELING_MODES = [];
export class ThresholdAnalyticUnit extends AnalyticUnit {
constructor(_serverObject?: any) {
@ -40,4 +42,8 @@ export class ThresholdAnalyticUnit extends AnalyticUnit {
set condition(val: Condition) { this._serverObject.condition = val; }
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>([
[DetectionStatus.READY, 'Detection is done'],
[DetectionStatus.RUNNING, 'Detection is running...'],
[DetectionStatus.FAILED, 'Detection failed']
[DetectionStatus.READY, '[DetectionStatus]: done'],
[DetectionStatus.RUNNING, '[DetectionStatus]: running...'],
[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">
<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-options="ds.id as ds.name for ds in ctrl.hasticDatasources"
ng-change="ctrl.onHasticDatasourceChange()"
@ -12,7 +12,7 @@
<div class="gf-form">
<div class="gf-form-button-row" ng-if="ctrl.analyticsController.serverStatus === false">
<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>
Reconnect to Hastic server
</button>
@ -20,213 +20,299 @@
<div ng-if="ctrl.analyticsController.serverStatus === true && !ctrl.analyticsController.loading">
<h5> Analytic Units </h5>
<div class="editor-row">
<div class="gf-form" ng-repeat="analyticUnit in ctrl.analyticsController.analyticUnits">
<label class="gf-form-label width-5">
<i class="fa fa-info" bs-tooltip="'Analytic unit id: ' + analyticUnit.id"></i>
&nbsp; Name
</label>
<input
type="text" class="gf-form-input max-width-15"
ng-model="analyticUnit.name"
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 ng-repeat="analyticUnit in ctrl.analyticsController.analyticUnits">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-5">
<i class="fa fa-info" bs-tooltip="'Analytic unit id: ' + analyticUnit.id"></i>
&nbsp; Name
</label>
<input
type="text" class="gf-form-input width-15"
ng-model="analyticUnit.name"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
>
</div>
<!--
<label class="gf-form-label width-6"> Confidence </label>
<input
type="number" class="gf-form-input width-5 ng-valid ng-scope ng-empty ng-dirty ng-valid-number ng-touched"
placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'"
data-placement="right" ng-model="ctrl.panel.decimals" ng-change="ctrl.render()" ng-model-onblur="" data-original-title="" title=""
/>
-->
<div class="gf-form">
<label class="gf-form-label width-5"> Type </label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input width-9"
ng-model="analyticUnit.type"
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>
<span class="gf-form-label">
<color-picker
color="analyticUnit.labeledColor"
onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id, false)"
/>
</span>
<div class="gf-form">
<label class="gf-form-label width-8"> Positive Color </label>
<span class="gf-form-label">
<color-picker
color="analyticUnit.labeledColor"
onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id, false)"
/>
</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 ng-if="analyticUnit.detectorType === 'pattern'"></div>
<div class="gf-form"
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>
<span ng-if="analyticUnit.detectorType === 'pattern'" class="gf-form-label">
<color-picker
color="analyticUnit.deletedColor"
onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id, true)"
<div class="gf-form" ng-if="analyticUnit.visible">
<!-- TODO: Remove hack with "margin-bottom: 0" -->
<gf-form-switch
class="gf-form"
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 -->
<select class="gf-form-input width-9"
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"
<div
class="gf-form"
ng-if="
analyticUnit.detectorType === 'threshold' &&
analyticUnit.condition !== 'NO_DATA'
analyticUnit.detectorType === 'pattern' ||
(analyticUnit.detectorType === 'anomaly' && analyticUnit.hasSeasonality)
"
type="number"
ng-model="analyticUnit.value"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
>
<label
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>
<input class="gf-form-input width-5"
ng-if="analyticUnit.detectorType === 'anomaly'"
min="0"
max="1"
type="number"
ng-model="analyticUnit.alpha"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
<div class="gf-form" ng-if="!analyticUnit.selected">
<label class="gf-form-label">
<a
ng-if="analyticUnit.visible"
bs-tooltip="'Hide. It`s visible now.'"
ng-click="ctrl.onToggleVisibility(analyticUnit.id)"
class="pointer"
>
<i class="fa fa-eye"></i>
</a>
<label class="gf-form-label width-6" ng-if="analyticUnit.detectorType === 'anomaly'"> Confidence </label>
<input class="gf-form-input width-5"
ng-if="analyticUnit.detectorType === 'anomaly'"
min="0"
type="number"
ng-model="analyticUnit.confidence"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
<a
ng-if="!analyticUnit.visible"
bs-tooltip="'Show. It`s hidden now.'"
ng-click="ctrl.onToggleVisibility(analyticUnit.id)"
class="pointer"
>
<i class="fa fa-eye-slash"></i>
</a>
</label>
</div>
<label class="gf-form-label" ng-if="analyticUnit.status === 'READY' && analyticUnit.visible">
Inspect
</label>
<div class="gf-form" ng-if="!analyticUnit.selected">
<a
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"
on-change="ctrl.onToggleInspect(analyticUnit.id)"
checked="analyticUnit.inspect"
/>
<div class="gf-form" ng-if="analyticUnit.selected">
<div class="gf-form-label">
<a
class="pointer"
ng-click="ctrl.onCancelLabeling(analyticUnit.id)"
>
Cancel
</a>
</div>
</div>
<label class="gf-form-label" ng-if="analyticUnit.visible">
HSR
</label>
<div class="gf-form">
<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"
on-change="ctrl.onToggleHSR(analyticUnit.id)"
checked="analyticUnit.showHSR"
/>
<div class="gf-form-inline">
<div class="gf-form width-20"/>
<!-- 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">
<a
ng-if="analyticUnit.visible"
bs-tooltip="'Hide. It`s visible now.'"
ng-click="ctrl.onToggleVisibility(analyticUnit.id)"
class="pointer"
>
<i class="fa fa-eye"></i>
</a>
<div class="gf-form" ng-if="analyticUnit.detectorType === 'anomaly'">
<label class="gf-form-label width-5"> Alpha </label>
<input class="gf-form-input width-9"
min="0"
max="1"
type="number"
ng-model="analyticUnit.alpha"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
</div>
<a
ng-if="!analyticUnit.visible"
bs-tooltip="'Show. It`s hidden now.'"
ng-click="ctrl.onToggleVisibility(analyticUnit.id)"
class="pointer"
>
<i class="fa fa-eye-slash"></i>
</a>
</label>
<div class="gf-form" ng-if="analyticUnit.detectorType === 'anomaly'">
<label class="gf-form-label width-6"> Confidence </label>
<input class="gf-form-input width-5"
min="0"
type="number"
ng-model="analyticUnit.confidence"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
</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"
ng-if="analyticUnit.detectorType === 'threshold' || analyticUnit.detectorType === 'anomaly'"
<div
class="gf-form"
ng-if="analyticUnit.detectorType === 'anomaly' && analyticUnit.hasSeasonality"
>
<a class="pointer" ng-click="ctrl.runDetect(analyticUnit.id)">
<i class="fa fa-spinner fa-spin" ng-if="analyticUnit.saving"></i>
<b ng-if="!analyticUnit.saving"> Detect </b>
<b ng-if="analyticUnit.saving" ng-disabled="true"> saving... </b>
</a>
</label>
<label class="gf-form-label width-9"> Seasonality Period </label>
<input
type="number" class="gf-form-input width-5"
ng-init="seasonalityValue = analyticUnit.seasonalityPeriod.value"
ng-model="seasonalityValue"
ng-blur="ctrl.onSeasonalityChange(analyticUnit.id, seasonalityValue)"
min="0"
>
</div>
<label
class="gf-form-label"
ng-if="analyticUnit.detectorType === 'pattern'"
ng-style="analyticUnit.status === 'LEARNING' && { 'cursor': 'not-allowed' }"
<div class="gf-form"
ng-if="
analyticUnit.detectorType === 'anomaly' &&
analyticUnit.hasSeasonality
"
>
<a class="pointer" tabindex="1"
ng-click="ctrl.onToggleLabelingMode(analyticUnit.id)"
ng-disabled="analyticUnit.status === 'LEARNING'"
<div class="gf-form-select-wrapper">
<!-- TODO: move periods from ng-options -->
<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>
<b ng-if="!analyticUnit.saving"> Save </b>
<b ng-if="analyticUnit.saving" ng-disabled="true"> saving... </b>
</a>
</label>
<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'"
/>
</button>
</div>
<label class="gf-form-label">
<a
ng-if="!analyticUnit.selected"
ng-click="ctrl.onRemove(analyticUnit.id)"
class="pointer"
<!-- TODO: Leave one Detect button instead of 2 -->
<div class="gf-form"
ng-if="
analyticUnit.detectorType === 'pattern' ||
(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>
</a>
<b> Detect </b>
</button>
</div>
<a
ng-if="analyticUnit.selected"
ng-click="ctrl.onCancelLabeling(analyticUnit.id)"
class="pointer"
<div class="gf-form"
ng-if="
analyticUnit.detectorType === 'threshold' ||
(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>
</a>
</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 === '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>
<b> Detect </b>
</button>
</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">
<label class="gf-form-label width-4"> Name </label>
<label class="gf-form-label width-5"> Name </label>
<input
type="text" class="gf-form-input max-width-15"
type="text" class="gf-form-input width-15"
ng-model="ctrl.analyticsController.newAnalyticUnit.name"
>
</div>
<div class="gf-form">
<label class="gf-form-label width-8"> Detector Type </label>
<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-options="analyticUnitDetectorType for analyticUnitDetectorType in ctrl.analyticUnitDetectorTypes"
ng-change="ctrl.analyticsController.onAnalyticUnitDetectorChange(ctrl.analyticUnitTypes);"
/>
</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">
<select class="gf-form-input width-10"
<select class="gf-form-input width-9"
ng-model="ctrl.analyticsController.newAnalyticUnit.type"
ng-options="
type.value as type.name
@ -234,23 +320,33 @@
"
/>
</div>
</div>
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.saveNew()">
<b ng-if="!ctrl.analyticsController.saving"> create </b>
<b ng-if="ctrl.analyticsController.saving" ng-disabled="true"> saving... </b>
</a>
</label>
<div class="gf-form">
<a class="btn btn-danger" ng-click="ctrl.cancelCreation()">
<b> Cancel </b>
</a>
</div>
<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 class="gf-form-button-row" ng-if="!ctrl.analyticsController.creatingAnalyticUnit">
<button class="btn btn-inverse width-12" ng-click="ctrl.createNew()">
<div class="gf-form-button-row">
<button
class="btn btn-secondary width-12"
ng-click="ctrl.createNew()"
ng-disabled="ctrl.analyticsController.creatingNew"
>
<i class="fa fa-plus"></i>
Add Analytic Unit
</button>
</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()">
Re-detect all analytic units
</button>

2
src/panel/graph_panel/series_overrides_ctrl.ts

@ -54,7 +54,7 @@ export class SeriesOverridesCtrl {
element: $element.find('.dropdown')[0],
position: 'top center',
openOn: 'click',
template: '<series-color-picker series="series" onColorChange="colorSelected" />',
template: '<series-color-picker-popover series="series" onColorChange="colorSelected" />',
model: {
autoClose: true,
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';
// TODO: TableTimeSeries is bad name
export type TableTimeSeries = {
values: [number, number][];
columns: string[];
};
export class AnalyticService {
private _isUp: boolean = false;
@ -201,10 +206,14 @@ export class AnalyticService {
}
async getHSR(analyticUnitId: AnalyticUnitId, from: number, to: number): Promise<{
values: [number, number][];
columns: string[];
}> {
hsr: TableTimeSeries,
lowerBound?: TableTimeSeries,
upperBound?: TableTimeSeries
} | null> {
const data = await this.get('/query', { analyticUnitId, from, to });
if(data === undefined) {
return null;
}
return data.results;
}
@ -242,7 +251,7 @@ export class AnalyticService {
} catch(error) {
// 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
if(error.xhrStatus !== 'complete') {
if(error.xhrStatus !== 'complete' || error.status === 502) {
this.displayConnectionErrorAlert();
this._isUp = false;
} else {

2
src/plugin.json

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

2
src/utlis.ts

@ -1,7 +1,7 @@
import url from 'url-parse';
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) {
if(!inputUrl) {

Loading…
Cancel
Save