Browse Source

Merge branch 'master' into hastic-datasource-#120

master
rozetko 5 years ago
parent
commit
88e1d10daa
  1. 12
      README.md
  2. 3
      package.json
  3. 25
      src/config/config_ctrl.ts
  4. 7
      src/config/template.html
  5. 2
      src/panel/graph_panel/axes_editor.ts
  6. 12
      src/panel/graph_panel/colors.ts
  7. 163
      src/panel/graph_panel/controllers/analytic_controller.ts
  8. 47
      src/panel/graph_panel/graph_ctrl.ts
  9. 55
      src/panel/graph_panel/graph_renderer.ts
  10. 8
      src/panel/graph_panel/graph_tooltip.ts
  11. 30
      src/panel/graph_panel/models/analytic_unit.ts
  12. 2
      src/panel/graph_panel/models/segment_array.ts
  13. 24
      src/panel/graph_panel/partials/help_section.html
  14. 92
      src/panel/graph_panel/partials/tab_analytics.html
  15. 4
      src/panel/graph_panel/plugin.json
  16. 16
      src/panel/graph_panel/services/analytic_service.ts
  17. 2
      src/plugin.json
  18. 29
      src/utlis.ts
  19. 12
      tests/analytic_controller.jest.ts
  20. 2
      tests/setup_tests.ts
  21. 26
      tests/utils.jest.ts

12
README.md

@ -14,11 +14,8 @@
A version of Grafana's default Graph Panel for rendering and labeling Hastic's patterns.
**Please note that we are still in alpha, so features are subject to change**
<img src="https://hastic.io/images/cpu_white.gif" />
See also:
* [Getting started](https://github.com/hastic/hastic-grafana-app/wiki/Getting-started)
* [Wiki](https://github.com/hastic/hastic-grafana-app/wiki)
@ -27,16 +24,11 @@ See also:
* [Installation from source](https://github.com/hastic/hastic-grafana-app/wiki/Installation-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)
## Support and Consulting
# Credits
Based on
* [grafana-plugin-template-webpack-typescript](https://github.com/CorpGlory/grafana-plugin-template-webpack-typescript)
* [@types/grafana](https://github.com/CorpGlory/types-grafana)
Commercial support, consulting, professional services — please send us your inquiry at ping@hastic.io

3
package.json

@ -1,6 +1,6 @@
{
"name": "grafana-hastic-app",
"version": "0.2.8",
"version": "0.3.0",
"description": "Hastic app: labeling and rendeting analytics from hastic-server",
"main": "dist/module",
"scripts": {
@ -41,6 +41,7 @@
"ts-jest": "^22.4.6",
"ts-loader": "^4.2.0",
"typescript": "^2.8.3",
"url-parse": "^1.4.4",
"webpack": "4.7.0",
"webpack-cli": "^2.1.2"
},

25
src/config/config_ctrl.ts

@ -1,13 +1,36 @@
import template from './template.html';
import { normalizeUrl } from '../utlis';
class ConfigCtrl {
static template = template;
appModel: any;
appEditCtrl: any;
constructor() {
if(this.appModel.jsonData === undefined) {
this.appModel.jsonData = {};
}
this.appEditCtrl.setPreUpdateHook(this.preUpdate.bind(this));
this.appEditCtrl.setPostUpdateHook(this.postUpdate.bind(this));
}
preUpdate() {
this.normalizeUrl();
return Promise.resolve();
}
postUpdate() {
// TODO: check whether hasticServerUrl is accessible
if(!this.appModel.enabled) {
return Promise.resolve();
}
return { message: 'Hastic app installed!' };
}
normalizeUrl() {
this.appModel.jsonData.hasticServerUrl = normalizeUrl(this.appModel.jsonData.hasticServerUrl);
}
}

7
src/config/template.html

@ -2,6 +2,11 @@
<div class="gf-form-group">
<div class="gf-form">
<label class="gf-form-label width-10">Hastic server url</label>
<input type="url" class="gf-form-input max-width-20" ng-model='ctrl.appModel.jsonData.hasticServerUrl'/>
<input
type="text"
class="gf-form-input max-width-20"
ng-model="ctrl.appModel.jsonData.hasticServerUrl"
ng-blur="ctrl.normalizeUrl"
/>
</div>
</div>

2
src/panel/graph_panel/axes_editor.ts

@ -82,7 +82,7 @@ export function axesEditorComponent() {
return {
restrict: 'E',
scope: true,
templateUrl: 'public/plugins/hastic-graph-panel/partials/axes_editor.html',
templateUrl: 'public/plugins/corpglory-hastic-app/panel/graph_panel/partials/axes_editor.html',
controller: AxesEditorCtrl,
};
}

12
src/panel/graph_panel/colors.ts

@ -76,6 +76,16 @@ export const ANALYTIC_UNIT_COLORS = [
'#f8c171',
];
export const DEFAULT_DELETED_SEGMENT_COLOR = '#00f0ff';
export const REGION_UNLABEL_COLOR_LIGHT = '#d1d1d1';
export const REGION_UNLABEL_COLOR_DARK = 'white';
export const LABELED_SEGMENT_BORDER_COLOR = 'black';
export const DELETED_SEGMENT_BORDER_COLOR = 'black';
export const SEGMENT_FILL_ALPHA = 0.5;
export const SEGMENT_STROKE_ALPHA = 0.8;
export const LABELING_MODE_ALPHA = 0.7;
export function hexToHsl(color) {
return tinycolor(color).toHsl();
}
@ -85,4 +95,4 @@ export function hslToHex(color) {
}
export default colors;
export default colors;

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

@ -4,7 +4,8 @@ import { AnalyticService } from '../services/analytic_service'
import {
AnalyticUnitId, AnalyticUnit,
AnalyticUnitsSet, AnalyticSegment, AnalyticSegmentsSearcher, AnalyticSegmentPair
AnalyticUnitsSet, AnalyticSegment, AnalyticSegmentsSearcher, AnalyticSegmentPair,
LabelingMode
} from '../models/analytic_unit';
import { MetricExpanded } from '../models/metric';
import { DatasourceRequest } from '../models/datasource';
@ -13,30 +14,29 @@ import { SegmentsSet } from '../models/segment_set';
import { SegmentArray } from '../models/segment_array';
import { ServerInfo } from '../models/info';
import { Threshold, Condition } from '../models/threshold';
import text from '../partials/help_section.html';
import { ANALYTIC_UNIT_COLORS } from '../colors';
import {
ANALYTIC_UNIT_COLORS,
LABELED_SEGMENT_BORDER_COLOR,
DELETED_SEGMENT_BORDER_COLOR,
SEGMENT_FILL_ALPHA,
SEGMENT_STROKE_ALPHA,
LABELING_MODE_ALPHA
} from '../colors';
import { Emitter } from 'grafana/app/core/utils/emitter';
import _ from 'lodash';
import * as tinycolor from 'tinycolor2';
export const REGION_FILL_ALPHA = 0.7;
export const REGION_STROKE_ALPHA = 0.9;
export const REGION_DELETE_COLOR_LIGHT = '#d1d1d1';
export const REGION_DELETE_COLOR_DARK = 'white';
const LABELED_SEGMENT_BORDER_COLOR = 'black';
const DELETED_SEGMENT_FILL_COLOR = 'black';
const DELETED_SEGMENT_BORDER_COLOR = 'black';
export class AnalyticController {
private _analyticUnitsSet: AnalyticUnitsSet;
private _selectedAnalyticUnitId: AnalyticUnitId = null;
private _labelingDataAddedSegments: SegmentsSet<AnalyticSegment>;
private _labelingDataDeletedSegments: SegmentsSet<AnalyticSegment>;
private _labelingDataRemovedSegments: SegmentsSet<AnalyticSegment>;
private _newAnalyticUnit: AnalyticUnit = null;
private _creatingNewAnalyticType: boolean = false;
private _savingNewAnalyticUnit: boolean = false;
@ -53,7 +53,7 @@ export class AnalyticController {
_panelObject.analyticUnits = _panelObject.anomalyTypes || [];
}
this._labelingDataAddedSegments = new SegmentArray<AnalyticSegment>();
this._labelingDataDeletedSegments = new SegmentArray<AnalyticSegment>();
this._labelingDataRemovedSegments = new SegmentArray<AnalyticSegment>();
this._analyticUnitsSet = new AnalyticUnitsSet(this._panelObject.analyticUnits);
this._thresholds = [];
this.updateThresholds();
@ -61,6 +61,8 @@ export class AnalyticController {
// this.analyticUnits.forEach(a => this.runEnabledWaiter(a));
}
get helpSectionText() { return text; }
getSegmentsSearcher(): AnalyticSegmentsSearcher {
return this._segmentsSearcher.bind(this);
}
@ -78,6 +80,7 @@ export class AnalyticController {
async sendThresholdParamsToServer(id: AnalyticUnitId) {
await this.saveThreshold(id);
await this._analyticService.runDetect(id);
await this._runStatusWaiter(this._analyticUnitsSet.byId(id));
}
@ -86,11 +89,11 @@ export class AnalyticController {
this._creatingNewAnalyticType = true;
this._savingNewAnalyticUnit = false;
if (this.analyticUnits.length === 0) {
this._newAnalyticUnit.color = ANALYTIC_UNIT_COLORS[0];
this._newAnalyticUnit.labeledColor = ANALYTIC_UNIT_COLORS[0];
} else {
let colorIndex = ANALYTIC_UNIT_COLORS.indexOf(_.last(this.analyticUnits).color) + 1;
let colorIndex = ANALYTIC_UNIT_COLORS.indexOf(_.last(this.analyticUnits).labeledColor) + 1;
colorIndex %= ANALYTIC_UNIT_COLORS.length;
this._newAnalyticUnit.color = ANALYTIC_UNIT_COLORS[colorIndex];
this._newAnalyticUnit.labeledColor = ANALYTIC_UNIT_COLORS[colorIndex];
}
}
@ -105,8 +108,9 @@ export class AnalyticController {
this._analyticUnitsSet.addItem(this._newAnalyticUnit);
this._creatingNewAnalyticType = false;
this._savingNewAnalyticUnit = false;
// this.runEnabledWaiter(this._newAnalyticUnit);
this._runStatusWaiter(this._newAnalyticUnit);
if(this._newAnalyticUnit.detectorType !== 'threshold') {
this._runStatusWaiter(this._newAnalyticUnit);
}
}
get creatingNew() { return this._creatingNewAnalyticType; }
@ -136,6 +140,7 @@ export class AnalyticController {
await this.disableLabeling();
this._selectedAnalyticUnitId = id;
this.labelingUnit.selected = true;
this.toggleLabelingMode(LabelingMode.LABELING);
this.toggleVisibility(id, true);
}
@ -160,33 +165,37 @@ export class AnalyticController {
this._labelingDataAddedSegments.getSegments().forEach(s => {
this.labelingUnit.segments.remove(s.id);
});
this._labelingDataDeletedSegments.getSegments().forEach(s => {
s.deleted = false;
this._labelingDataRemovedSegments.getSegments().forEach(s => {
this.labelingUnit.segments.addSegment(s);
});
this.dropLabeling();
}
dropLabeling() {
this._labelingDataAddedSegments.clear();
this._labelingDataDeletedSegments.clear();
this._labelingDataRemovedSegments.clear();
this.labelingUnit.selected = false;
this._selectedAnalyticUnitId = null;
this._tempIdCounted = -1;
}
get labelingMode(): boolean {
get inLabelingMode(): boolean {
return this._selectedAnalyticUnitId !== null;
}
get labelingDeleteMode(): boolean {
if(!this.labelingMode) {
return false;
get labelingMode(): LabelingMode {
if(!this.inLabelingMode) {
return LabelingMode.NOT_IN_LABELING_MODE;
}
return this.labelingUnit.deleteMode;
return this.labelingUnit.labelingMode;
}
addLabelSegment(segment: Segment, deleted?: boolean) {
var asegment = this.labelingUnit.addLabeledSegment(segment, deleted);
set labelingMode(labelingMode: LabelingMode) {
this.labelingUnit.labelingMode = labelingMode;
}
addLabelSegment(segment: Segment, deleted = false) {
const asegment = this.labelingUnit.addLabeledSegment(segment, deleted);
this._labelingDataAddedSegments.addSegment(asegment);
}
@ -194,11 +203,15 @@ export class AnalyticController {
return this._analyticUnitsSet.items;
}
onAnalyticUnitColorChange(id: AnalyticUnitId, value: string) {
onAnalyticUnitColorChange(id: AnalyticUnitId, value: string, deleted: boolean) {
if(id === undefined) {
throw new Error('id is undefined');
}
this._analyticUnitsSet.byId(id).color = value;
if(deleted) {
this._analyticUnitsSet.byId(id).deletedColor = value;
} else {
this._analyticUnitsSet.byId(id).labeledColor = value;
}
}
fetchAnalyticUnitsStatuses() {
@ -227,7 +240,7 @@ export class AnalyticController {
var allSegmentsSet = new SegmentArray(allSegmentsList);
if(analyticUnit.selected) {
this._labelingDataAddedSegments.getSegments().forEach(s => allSegmentsSet.addSegment(s));
this._labelingDataDeletedSegments.getSegments().forEach(s => allSegmentsSet.remove(s.id));
this._labelingDataRemovedSegments.getSegments().forEach(s => allSegmentsSet.remove(s.id));
}
analyticUnit.segments = allSegmentsSet;
}
@ -240,72 +253,68 @@ export class AnalyticController {
if(
this._labelingDataAddedSegments.length === 0 &&
this._labelingDataDeletedSegments.length === 0
this._labelingDataRemovedSegments.length === 0
) {
return [];
}
await this._analyticService.updateMetric(unit.id, this._currentMetric, this._currentDatasource);
return this._analyticService.updateSegments(
unit.id, this._labelingDataAddedSegments, this._labelingDataDeletedSegments
const newIds = await this._analyticService.updateSegments(
unit.id, this._labelingDataAddedSegments, this._labelingDataRemovedSegments
);
if(unit.labelingMode !== LabelingMode.UNLABELING) {
await this._analyticService.runDetect(unit.id);
}
return newIds;
}
// TODO: move to renderer
updateFlotEvents(isEditMode, options) {
updateFlotEvents(isEditMode: boolean, options: any) {
if(options.grid.markings === undefined) {
options.markings = [];
}
for(var i = 0; i < this.analyticUnits.length; i++) {
var analyticUnit = this.analyticUnits[i];
var borderColor = addAlphaToRGB(analyticUnit.color, REGION_STROKE_ALPHA);
var fillColor = addAlphaToRGB(analyticUnit.color, REGION_FILL_ALPHA);
var segments = analyticUnit.segments.getSegments();
const analyticUnit = this.analyticUnits[i];
if(!analyticUnit.visible) {
continue;
}
if(isEditMode && this.labelingMode) {
if(analyticUnit.selected) {
borderColor = addAlphaToRGB(borderColor, 0.7);
fillColor = addAlphaToRGB(borderColor, 0.7);
} else {
continue;
}
}
var rangeDist = +options.xaxis.max - +options.xaxis.min;
let defaultBorderColor = addAlphaToRGB(analyticUnit.labeledColor, SEGMENT_STROKE_ALPHA);
let defaultFillColor = addAlphaToRGB(analyticUnit.labeledColor, SEGMENT_FILL_ALPHA);
let labeledSegmentBorderColor = tinycolor(LABELED_SEGMENT_BORDER_COLOR).toRgbString();
labeledSegmentBorderColor = addAlphaToRGB(labeledSegmentBorderColor, REGION_STROKE_ALPHA);
let deletedSegmentFillColor = tinycolor(DELETED_SEGMENT_FILL_COLOR).toRgbString();
deletedSegmentFillColor = addAlphaToRGB(deletedSegmentFillColor, REGION_STROKE_ALPHA);
labeledSegmentBorderColor = addAlphaToRGB(labeledSegmentBorderColor, SEGMENT_STROKE_ALPHA);
let deletedSegmentFillColor = tinycolor(analyticUnit.deletedColor).toRgbString();
deletedSegmentFillColor = addAlphaToRGB(deletedSegmentFillColor, SEGMENT_FILL_ALPHA);
let deletedSegmentBorderColor = tinycolor(DELETED_SEGMENT_BORDER_COLOR).toRgbString();
deletedSegmentBorderColor = addAlphaToRGB(deletedSegmentBorderColor, REGION_STROKE_ALPHA);
deletedSegmentBorderColor = addAlphaToRGB(deletedSegmentBorderColor, SEGMENT_STROKE_ALPHA);
if(isEditMode && this.inLabelingMode && analyticUnit.selected) {
defaultBorderColor = addAlphaToRGB(defaultBorderColor, LABELING_MODE_ALPHA);
defaultFillColor = addAlphaToRGB(defaultFillColor, LABELING_MODE_ALPHA);
labeledSegmentBorderColor = addAlphaToRGB(labeledSegmentBorderColor, LABELING_MODE_ALPHA);
deletedSegmentFillColor = addAlphaToRGB(deletedSegmentFillColor, LABELING_MODE_ALPHA);
deletedSegmentBorderColor = addAlphaToRGB(deletedSegmentBorderColor, LABELING_MODE_ALPHA);
}
const segments = analyticUnit.segments.getSegments();
const rangeDist = +options.xaxis.max - +options.xaxis.min;
segments.forEach(s => {
let segmentBorderColor;
let segmentFillColor = fillColor;
let segmentBorderColor = defaultBorderColor;
let segmentFillColor = defaultFillColor;
if(this.labelingDeleteMode) {
if(s.deleted) {
segmentBorderColor = deletedSegmentBorderColor;
segmentFillColor = deletedSegmentFillColor;
}
if(s.deleted) {
segmentBorderColor = deletedSegmentBorderColor;
segmentFillColor = deletedSegmentFillColor;
} else {
if(s.deleted) {
return;
if(s.labeled) {
segmentBorderColor = labeledSegmentBorderColor;
}
}
if(s.labeled) {
segmentBorderColor = labeledSegmentBorderColor;
} else {
segmentBorderColor = borderColor;
}
var expanded = s.expandDist(rangeDist, 0.01);
const expanded = s.expandDist(rangeDist, 0.01);
options.grid.markings.push({
xaxis: { from: expanded.from, to: expanded.to },
color: segmentFillColor
@ -324,20 +333,24 @@ export class AnalyticController {
}
deleteLabelingAnalyticUnitSegmentsInRange(from: number, to: number) {
var allRemovedSegs = this.labelingUnit.removeSegmentsInRange(from, to);
const allRemovedSegs = this.labelingUnit.removeSegmentsInRange(from, to);
allRemovedSegs.forEach(s => {
if(!this._labelingDataAddedSegments.has(s.id)) {
this._labelingDataDeletedSegments.addSegment(s);
this._labelingDataRemovedSegments.addSegment(s);
}
});
this._labelingDataAddedSegments.removeInRange(from, to);
}
toggleDeleteMode() {
if(!this.labelingMode) {
throw new Error('Cant enter delete mode is labeling mode disabled');
toggleLabelingMode(labelingMode: LabelingMode) {
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.deleteMode = !this.labelingUnit.deleteMode;
}
async removeAnalyticUnit(id: AnalyticUnitId, silent: boolean = false) {

47
src/panel/graph_panel/graph_ctrl.ts

@ -7,7 +7,7 @@ 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 { AnalyticUnitId, AnalyticUnit, LabelingMode } from './models/analytic_unit';
import { AnalyticService } from './services/analytic_service';
import { AnalyticController } from './controllers/analytic_controller';
import { PanelInfo } from './models/info';
@ -35,7 +35,6 @@ class GraphCtrl extends MetricsPanelCtrl {
private _datasourceRequest: DatasourceRequest;
private _datasources: any;
private _panelPath: any;
private _renderError: boolean = false;
// annotationsPromise: any;
@ -157,19 +156,28 @@ class GraphCtrl extends MetricsPanelCtrl {
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
}
rebindDKey() {
rebindKeys() {
const dKeyCode = 68;
const uKeyCode = 85;
$(document).off('keydown.hasticDKey');
$(document).on('keydown.hasticDKey', (e) => {
// 68 is 'd' key kode
if(e.keyCode === 68) {
if(e.keyCode === dKeyCode) {
this.onDKey();
}
});
$(document).off('keydown.hasticUKey');
$(document).on('keydown.hasticUKey', (e) => {
if(e.keyCode === uKeyCode) {
this.onUKey();
}
});
}
editPanel() {
super.editPanel();
this.rebindDKey();
this.rebindKeys();
}
async getBackendURL(): Promise<string> {
@ -264,6 +272,9 @@ class GraphCtrl extends MetricsPanelCtrl {
await this.analyticsController.removeAnalyticUnit(analyticUnit.id, true);
}
if(analyticUnit.status === 'READY') {
if(this.range === undefined) {
this.updateTimeRange();
}
await this.analyticsController.fetchSegments(analyticUnit, +this.range.from, +this.range.to);
}
this.render(this.seriesList);
@ -300,7 +311,7 @@ class GraphCtrl extends MetricsPanelCtrl {
onInitEditMode() {
this.rebindDKey(); // a small hask: bind if we open page in edit mode
this.rebindKeys(); // a small hask: bind if we open page in edit mode
const partialPath = this.panelPath + '/partials';
this.addEditorTab('Analytics', `${partialPath}/tab_analytics.html`, 2);
@ -581,11 +592,11 @@ class GraphCtrl extends MetricsPanelCtrl {
this.analyticsController.fetchAnalyticUnitName(analyticUnit);
}
onColorChange(id: AnalyticUnitId, value: string) {
onColorChange(id: AnalyticUnitId, deleted: boolean, value: string) {
if(id === undefined) {
throw new Error('id is undefined');
}
this.analyticsController.onAnalyticUnitColorChange(id, value);
this.analyticsController.onAnalyticUnitColorChange(id, value, deleted);
this.render();
}
@ -624,10 +635,18 @@ class GraphCtrl extends MetricsPanelCtrl {
}
onDKey() {
if(!this.analyticsController.labelingMode) {
if(!this.analyticsController.inLabelingMode) {
return;
}
this.analyticsController.toggleDeleteMode();
this.analyticsController.toggleLabelingMode(LabelingMode.DELETING);
this.refresh();
}
onUKey() {
if(!this.analyticsController.inLabelingMode) {
return;
}
this.analyticsController.toggleLabelingMode(LabelingMode.UNLABELING);
this.refresh();
}
@ -640,8 +659,12 @@ class GraphCtrl extends MetricsPanelCtrl {
const datasource = await this._getDatasourceByName(this.panel.datasource);
const backendUrl = await this.getBackendURL();
let grafanaVersion = 'unknown';
if(_.has(window, 'grafanaBootData.settings.buildInfo.version')) {
grafanaVersion = window.grafanaBootData.settings.buildInfo.version;
}
this._panelInfo = {
grafanaVersion: this.contextSrv.version,
grafanaVersion,
grafanaUrl: window.location.host,
datasourceType: datasource.type,
hasticServerUrl: backendUrl

55
src/panel/graph_panel/graph_renderer.ts

@ -1,15 +1,17 @@
import { Segment } from './models/segment';
import { LabelingMode } from './models/analytic_unit';
import { GraphTooltip } from './graph_tooltip';
import { convertValuesToHistogram, getSeriesValues } from './histogram';
import {
AnalyticController,
REGION_FILL_ALPHA,
REGION_STROKE_ALPHA,
REGION_DELETE_COLOR_LIGHT,
REGION_DELETE_COLOR_DARK
AnalyticController
} from './controllers/analytic_controller';
import {
REGION_UNLABEL_COLOR_LIGHT,
REGION_UNLABEL_COLOR_DARK
} from './colors';
import { GraphCtrl } from './graph_ctrl';
import 'jquery';
@ -140,20 +142,26 @@ export class GraphRenderer {
if(this._isHasticEvent(selectionEvent)) {
this.plot.clearSelection();
var id = this._analyticController.getNewTempSegmentId();
var segment = new Segment(
const id = this._analyticController.getNewTempSegmentId();
const segment = new Segment(
id,
Math.round(selectionEvent.xaxis.from),
Math.round(selectionEvent.xaxis.to)
);
if(this._analyticController.labelingDeleteMode) {
if(this._analyticController.labelingMode === LabelingMode.DELETING) {
this._analyticController.deleteLabelingAnalyticUnitSegmentsInRange(
segment.from, segment.to
);
this._analyticController.addLabelSegment(segment, true);
}
if(this._analyticController.labelingMode === LabelingMode.LABELING) {
this._analyticController.addLabelSegment(segment, false);
}
this._analyticController.addLabelSegment(
segment, this._analyticController.labelingDeleteMode
);
if(this._analyticController.labelingMode === LabelingMode.UNLABELING) {
this._analyticController.deleteLabelingAnalyticUnitSegmentsInRange(
segment.from, segment.to
);
}
this.renderPanel();
return;
@ -336,21 +344,22 @@ export class GraphRenderer {
}
private _chooseSelectionColor(e) {
var color = COLOR_SELECTION;
var fillAlpha = 0.4;
var strokeAlpha = 0.4;
let color = COLOR_SELECTION;
if(this._isHasticEvent(e)) {
if(this._analyticController.labelingDeleteMode) {
if(this._analyticController.labelingMode === LabelingMode.DELETING) {
color = this._analyticController.labelingUnit.deletedColor;
}
if(this._analyticController.labelingMode === LabelingMode.LABELING) {
color = this._analyticController.labelingUnit.labeledColor;
}
if(this._analyticController.labelingMode === LabelingMode.UNLABELING) {
color = this.contextSrv.user.lightTheme ?
REGION_DELETE_COLOR_LIGHT :
REGION_DELETE_COLOR_DARK;
} else {
color = this._analyticController.labelingUnit.color;
REGION_UNLABEL_COLOR_LIGHT :
REGION_UNLABEL_COLOR_DARK;
}
fillAlpha = REGION_FILL_ALPHA;
strokeAlpha = REGION_STROKE_ALPHA;
}
this.plot.getOptions().selection.color = color
this.plot.getOptions().selection.color = color;
}
private _buildFlotPairs(data) {
@ -807,7 +816,7 @@ export class GraphRenderer {
private _isHasticEvent(obj: any) {
return (obj.ctrlKey || obj.metaKey) &&
this.contextSrv.isEditor &&
this._analyticController.labelingMode;
this._analyticController.inLabelingMode;
}
}

8
src/panel/graph_panel/graph_tooltip.ts

@ -200,12 +200,8 @@ export class GraphTooltip {
var from = this.dashboard.formatDate(s.segment.from, 'HH:mm:ss.SSS');
var to = this.dashboard.formatDate(s.segment.to, 'HH:mm:ss.SSS');
if(s.segment.deleted && !s.analyticUnit.deleteMode) {
return;
}
let icon;
if (s.segment.labeled) {
if(s.segment.labeled) {
icon = 'fa-thumb-tack';
} else if (s.segment.deleted) {
icon = 'fa-trash';
@ -215,7 +211,7 @@ export class GraphTooltip {
result += `
<div class="graph-tooltip-list-item">
<div class="graph-tooltip-series-name">
<i class="fa fa-exclamation" style="color:${s.analyticUnit.color}"></i>
<i class="fa fa-exclamation" style="color:${s.analyticUnit.labeledColor}"></i>
${s.analyticUnit.name}:
</div>
<div class="graph-tooltip-value">

30
src/panel/graph_panel/models/analytic_unit.ts

@ -2,11 +2,18 @@ import { SegmentsSet } from './segment_set';
import { SegmentArray } from './segment_array';
import { Segment, SegmentId } from './segment';
import { ANALYTIC_UNIT_COLORS } from '../colors';
import { ANALYTIC_UNIT_COLORS, DEFAULT_DELETED_SEGMENT_COLOR } from '../colors';
import _ from 'lodash';
export enum LabelingMode {
LABELING = 'LABELING',
UNLABELING = 'UNLABELING',
DELETING = 'DELETING',
NOT_IN_LABELING_MODE = 'NOT_IN_LABELING_MODE'
};
export type AnalyticSegmentPair = { analyticUnit: AnalyticUnit, segment: AnalyticSegment };
export type AnalyticSegmentsSearcher = (point: number, rangeDist: number) => AnalyticSegmentPair[];
@ -23,8 +30,8 @@ export class AnalyticSegment extends Segment {
export class AnalyticUnit {
private _labelingMode: LabelingMode = LabelingMode.LABELING;
private _selected: boolean = false;
private _deleteMode: boolean = false;
private _saving: boolean = false;
private _segmentSet = new SegmentArray<AnalyticSegment>();
private _status: string;
@ -36,7 +43,8 @@ export class AnalyticUnit {
}
_.defaults(this._panelObject, {
name: 'AnalyticUnitName',
color: ANALYTIC_UNIT_COLORS[0],
labeledColor: ANALYTIC_UNIT_COLORS[0],
deletedColor: DEFAULT_DELETED_SEGMENT_COLOR,
detectorType: 'pattern',
type: 'GENERAL',
alert: false
@ -58,8 +66,11 @@ export class AnalyticUnit {
set confidence(value: number) { this._panelObject.confidence = value; }
get confidence(): number { return this._panelObject.confidence; }
set color(value: string) { this._panelObject.color = value; }
get color(): string { return this._panelObject.color; }
set labeledColor(value: string) { this._panelObject.labeledColor = value; }
get labeledColor(): string { return this._panelObject.labeledColor; }
set deletedColor(value: string) { this._panelObject.deletedColor = value; }
get deletedColor(): string { return this._panelObject.deletedColor; }
set alert(value: boolean) { this._panelObject.alert = value; }
get alert(): boolean { return this._panelObject.alert; }
@ -67,8 +78,8 @@ export class AnalyticUnit {
get selected(): boolean { return this._selected; }
set selected(value: boolean) { this._selected = value; }
get deleteMode(): boolean { return this._deleteMode; }
set deleteMode(value: boolean) { this._deleteMode = value; }
get labelingMode(): LabelingMode { return this._labelingMode; }
set labelingMode(value: LabelingMode) { this._labelingMode = value; }
get saving(): boolean { return this._saving; }
set saving(value: boolean) { this._saving = value; }
@ -81,16 +92,13 @@ export class AnalyticUnit {
}
addLabeledSegment(segment: Segment, deleted: boolean): AnalyticSegment {
var asegment = new AnalyticSegment(true, segment.id, segment.from, segment.to, deleted);
const asegment = new AnalyticSegment(!deleted, segment.id, segment.from, segment.to, deleted);
this._segmentSet.addSegment(asegment);
return asegment;
}
removeSegmentsInRange(from: number, to: number): AnalyticSegment[] {
let deletedSegments = this._segmentSet.removeInRange(from, to);
deletedSegments.forEach(s => {
s.deleted = true;
});
return deletedSegments;
}

2
src/panel/graph_panel/models/segment_array.ts

@ -47,7 +47,7 @@ export class SegmentArray<T extends Segment> implements SegmentsSet<T> {
findSegments(point: number, rangeDist: number): T[] {
return this._segments.filter(s => {
var expanded = s.expandDist(rangeDist, 0.01);
const expanded = s.expandDist(rangeDist, 0.01);
return (expanded.from <= point) && (point <= expanded.to);
});
}

24
src/panel/graph_panel/partials/help_section.html

@ -0,0 +1,24 @@
<pre>
<b>For usage instructions:</b> <a class="text-link" href="https://github.com/hastic/hastic-grafana-app/wiki/Analytic-units">Visit our <b>Wiki</b> page</a>.
<b>If you encounter any problems:</b>
Look for solution or create a new Issue <a href="https://github.com/hastic/hastic-grafana-app/issues"><b>here</b></a>.
<b>Available labeling patterns examples:</b>
1) General: patterns in your data that don't fall under any of provided built-in patterns.
2) Peaks: a sharp increase to a certain single value, followed by a return to the original value.
<!-- TODO use ng-src="ctrl.pluginPath/..." instead -->
<img src="public/plugins/corpglory-hastic-app/img/peaks.jpg">
3) Troughs: a sharp decrease to a certain single value, followed by a return to the original value.
<img src="public/plugins/corpglory-hastic-app/img/troughs.jpg">
4) Jumps: increase to a certain value without returning to the original state.
<img src="public/plugins/corpglory-hastic-app/img/jumps.jpg">
5) Drops: decrease to a certain value without returning to the original state.
<img src="public/plugins/corpglory-hastic-app/img/drops.jpg">
6) Custom: any custom model created and imported by you.
</pre>

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

@ -11,7 +11,7 @@
<div class="editor-row">
<div class="gf-form" ng-repeat="analyticUnit in ctrl.analyticsController.analyticUnits">
<label class="gf-form-label width-6">
<label class="gf-form-label width-5">
<i class="fa fa-info" bs-tooltip="'Analytic unit id: ' + analyticUnit.id"></i>
&nbsp; Name
</label>
@ -21,9 +21,9 @@
ng-change="ctrl.onAnalyticUnitNameChange(analyticUnit)"
>
<label class="gf-form-label width-8"> Type </label>
<label class="gf-form-label width-4"> Type </label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input width-12"
<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"
@ -39,11 +39,22 @@
/>
-->
<label class="gf-form-label width-6"> Color </label>
<label class="gf-form-label width-8"> Positive Color </label>
<span class="gf-form-label">
<color-picker
color="analyticUnit.color"
onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id)"
color="analyticUnit.labeledColor"
onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id, false)"
/>
</span>
<!-- 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>
<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)"
/>
</span>
@ -58,12 +69,21 @@
>
<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.selected && !analyticUnit.deleteMode && !analyticUnit.saving"> labeling </b>
<b ng-if="analyticUnit.selected && analyticUnit.deleteMode && !analyticUnit.saving"> deleting </b>
<b ng-if="analyticUnit.saving" ng-disabled="true"> saving... </b>
</a>
</label>
<select class="gf-form-input width-10"
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'"
/>
<select class="gf-form-input width-7"
ng-model="ctrl.analyticsController.getThreshold(analyticUnit.id).condition"
ng-options="type for type in ctrl.analyticsController.conditions"
@ -79,19 +99,18 @@
"
/>
<!-- TODO set .saving flag to thresholds, when learning is in progress -->
<button
class="btn btn-inverse"
ng-if="analyticUnit.detectorType === 'threshold'"
<button
class="btn btn-inverse"
ng-if="analyticUnit.detectorType === 'threshold'"
ng-click="ctrl.analyticsController.sendThresholdParamsToServer(analyticUnit.id)"
ng-disabled="analyticUnit.status === 'PENDING' || analyticUnit.status === 'LEARNING'"
>
Apply
</button>
<label class="gf-form-label">
<label class="gf-form-label" ng-hide="analyticUnit.selected">
<a
ng-if="analyticUnit.visible"
ng-disabled="analyticUnit.selected"
bs-tooltip="'Hide. It`s visible now.'"
ng-click="ctrl.onToggleVisibility(analyticUnit.id)"
class="pointer"
@ -101,7 +120,6 @@
<a
ng-if="!analyticUnit.visible"
ng-disabled="analyticUnit.selected"
bs-tooltip="'Show. It`s hidden now.'"
ng-click="ctrl.onToggleVisibility(analyticUnit.id)"
class="pointer"
@ -147,7 +165,7 @@
<label class="gf-form-label width-8"> Detector Type </label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input width-12"
<select class="gf-form-input width-10"
ng-model="ctrl.analyticsController.newAnalyticUnit.detectorType"
ng-options="analyticUnitDetectorType for analyticUnitDetectorType in ctrl.analyticUnitDetectorTypes"
ng-change="ctrl.analyticsController.onAnalyticUnitDetectorChange(ctrl.analyticUnitTypes);"
@ -156,7 +174,7 @@
<label class="gf-form-label width-8"> Type </label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input width-12"
<select class="gf-form-input width-10"
ng-model="ctrl.analyticsController.newAnalyticUnit.type"
ng-options="
type.value as type.name
@ -188,45 +206,5 @@
</button>
</div>
<div class="gf-form" ng-show="ctrl.showHelp" >
<pre class="gf-form-pre alert alert-info">
<b>Adding Analytic Units:</b>
1) Click on 'Add Analytic Unit' button to add an analytic unit.
2) Type in desired analytic unit name into provided 'Name' field.
3) Choose pattern type from the 'Type' dropdown menu. Pattern types explained below.
4) Click 'create' button.
<b>Labeling segments:</b>
1) Click on the chart symbol <img ng-src="{{ctrl.pluginPath}}/img/chartSymbol.png"> to enter labeling mode.
2) Hold down 'Ctrl' button on your keyboard and hold down left mouse button while dragging to label segments manually. Release LMB to finish labeling the segment.
3) After all of the needed segments are labeled click on the chart symbol again to exit labeling mode.
<b>Hastic will now start learning, and after a short while you will see the results on your graph.</b>
<b>Deleting segments:</b>
1) Click on the chart symbol to enter labeling mode. (skip this step if you are already in labeling mode)
2) Press 'D' key twice to switch from <img ng-src="{{ctrl.pluginPath}}/img/labeling.jpg"> to <img ng-src="{{ctrl.pluginPath}}/img/deleting.jpg">.
3) Hold down 'Ctrl' button on your keyboard and hold down left mouse button while dragging to mark segments for deletion.
4) After all of the needed segments are deleted click on the chart symbol again to exit labeling mode.
<b>Hastic will now start learning again, based on updated segments list.</b>
<b>Available labeling patterns:</b>
1) General: patterns in your data that don't fall under any of provided built-in patterns.
2) Peaks: a sharp increase to a certain single value, followed by a return to the original value.
<img ng-src="{{ctrl.pluginPath}}/img/peaks.jpg">
3) Troughs: a sharp decrease to a certain single value, followed by a return to the original value.
<img ng-src="{{ctrl.pluginPath}}/img/troughs.jpg">
4) Jumps: increase to a certain value without returning to the original state.
<img ng-src="{{ctrl.pluginPath}}/img/jumps.jpg">
5) Drops: decrease to a certain value without returning to the original state.
<img ng-src="{{ctrl.pluginPath}}/img/drops.jpg">
6) Custom: any custom model created and imported by you.
</pre>
</div>
<div class="gf-form" ng-show="ctrl.showHelp" ng-bind-html="ctrl.analyticsController.helpSectionText"></div>

4
src/panel/graph_panel/plugin.json

@ -4,8 +4,8 @@
"id": "corpglory-hastic-graph-panel",
"info": {
"logos": {
"small": "corpglory-hastic-app/img/icn-graph-panel.png",
"large": "corpglory-hastic-app/img/icn-graph-panel.png"
"small": "../corpglory-hastic-app/img/icn-graph-panel.png",
"large": "../corpglory-hastic-app/img/icn-graph-panel.png"
}
}
}

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

@ -66,20 +66,22 @@ export class AnalyticService {
}
async updateSegments(
id: AnalyticUnitId, addedSegments: SegmentsSet<Segment>, removedSegments: SegmentsSet<Segment>
id: AnalyticUnitId, addedSegments: SegmentsSet<AnalyticSegment>, removedSegments: SegmentsSet<AnalyticSegment>
): Promise<SegmentId[]> {
const getJSONs = (segs: SegmentsSet<Segment>) => segs.getSegments().map(segment => ({
const getJSONs = (segs: SegmentsSet<AnalyticSegment>) => segs.getSegments().map(segment => ({
from: segment.from,
to: segment.to
to: segment.to,
labeled: segment.labeled,
deleted: segment.deleted
}));
var payload = {
const payload = {
id,
addedSegments: getJSONs(addedSegments),
removedSegments: removedSegments.getSegments().map(s => s.id)
};
var data = await this.patch('/segments', payload);
const data = await this.patch('/segments', payload);
if(data.addedIds === undefined) {
throw new Error('Server didn`t send addedIds');
}
@ -158,6 +160,10 @@ export class AnalyticService {
return this.patch('/analyticUnits', updateObj);
}
async runDetect(id: AnalyticUnitId) {
return this.post('/analyticUnits/detect', { id });
}
private async _analyticRequest(method: string, url: string, data?: any) {
try {
method = method.toUpperCase();

2
src/plugin.json

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

29
src/utlis.ts

@ -0,0 +1,29 @@
import url from 'url-parse';
export function normalizeUrl(grafanaUrl: string) {
if(!grafanaUrl) {
return grafanaUrl;
}
let urlObj = new url(grafanaUrl, {});
if(urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
grafanaUrl = `http://${grafanaUrl}`;
urlObj = new url(grafanaUrl, {});
console.log('No protocol provided in GRAFANA_URL -> inserting "http://"');
}
if(urlObj.slashes === false) {
urlObj = new url(`${urlObj.protocol}//${urlObj.pathname}`, {});
console.log('No slashes were provided after the protocol -> inserting slashes');
}
if(urlObj.pathname.slice(-1) === '/') {
urlObj.pathname = urlObj.pathname.slice(0, -1);
console.log('Removing the slash at the end of GRAFANA_URL');
}
let finalUrl = `${urlObj.protocol}//${urlObj.hostname}`;
if(urlObj.port !== '') {
finalUrl = finalUrl + ':' + urlObj.port;
}
if(urlObj.pathname !== '') {
finalUrl = finalUrl + urlObj.pathname;
}
return finalUrl;
}

12
tests/analytic_controller.jest.ts

@ -9,7 +9,7 @@ describe('AnalyticController', function () {
it('should create analytic units with colors from palette', async function () {
for (let color of ANALYTIC_UNIT_COLORS) {
analyticController.createNew();
expect(analyticController.newAnalyticUnit.color).toBe(color);
expect(analyticController.newAnalyticUnit.labeledColor).toBe(color);
await analyticController.saveNew({} as MetricExpanded, {} as DatasourceRequest, '');
}
});
@ -29,19 +29,19 @@ describe('AnalyticController', function () {
let auArray = analyticController.analyticUnits;
analyticController.createNew();
await analyticController.saveNew({} as MetricExpanded, {} as DatasourceRequest, '');
expect(auArray[auArray.length - 2].panelObject.color).not.toBe(auArray[auArray.length - 1].panelObject.color);
expect(auArray[auArray.length - 2].panelObject.labeledColor).not.toBe(auArray[auArray.length - 1].panelObject.labeledColor);
});
it('should set different color to newly created Analytic Unit, afer LAST AU was deleted', async function () {
it('should set different color to newly created Analytic Unit, after LAST AU was deleted', async function () {
let auArray = analyticController.analyticUnits;
auArray.splice(-1, 1);
analyticController.createNew();
await analyticController.saveNew({} as MetricExpanded, {} as DatasourceRequest, '');
expect(auArray[auArray.length - 2].panelObject.color).not.toBe(auArray[auArray.length - 1].panelObject.color);
expect(auArray[auArray.length - 2].panelObject.labeledColor).not.toBe(auArray[auArray.length - 1].panelObject.labeledColor);
});
it('should change color on choosing from palette', function () {
analyticController.onAnalyticUnitColorChange('1', 'red');
expect(analyticController.analyticUnits[0].color).toBe('red');
analyticController.onAnalyticUnitColorChange('1', 'red', false);
expect(analyticController.analyticUnits[0].labeledColor).toBe('red');
});
});

2
tests/setup_tests.ts

@ -25,5 +25,7 @@ analyticService.postNewItem = async function (newItem: AnalyticUnit, metric: Met
export const analyticController = new AnalyticController({}, analyticService, new Emitter());
jest.mock('../src/panel/graph_panel/partials/help_section.html', () => '');
console.log = jest.fn();
console.error = jest.fn();

26
tests/utils.jest.ts

@ -0,0 +1,26 @@
import { normalizeUrl } from '../src/utlis';
describe('Normalize URL', function() {
const cases = [
{ value: '127.0.0.1:8000', expected: 'http://127.0.0.1:8000' },
{ value: '127.0.0.1:8000/', expected: 'http://127.0.0.1:8000' },
{ value: 'localhost:8000', expected: 'http://localhost:8000' },
{ value: 'localhost:8000/', expected: 'http://localhost:8000' },
{ value: 'http://localhost:3000', expected: 'http://localhost:3000' },
{ value: 'http://localhost:3000/', expected: 'http://localhost:3000' },
{ value: 'https://localhost:8000', expected: 'https://localhost:8000' },
{ value: 'https://localhost:8000/', expected: 'https://localhost:8000' },
{ value: 'http://example.com', expected: 'http://example.com' },
{ value: 'http://example.com/', expected: 'http://example.com' },
{ value: 'https://example.com', expected: 'https://example.com' },
{ value: 'https://example.com/', expected: 'https://example.com' },
{ value: 'https://example.com/grafana', expected: 'https://example.com/grafana' },
{ value: 'https://example.com/grafana/', expected: 'https://example.com/grafana' },
];
it('should normalize URLs correctly', function() {
cases.forEach(testCase => {
expect(normalizeUrl(testCase.value)).toBe(testCase.expected);
});
});
});
Loading…
Cancel
Save