You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

533 lines
17 KiB

// Corresponds to https://github.com/hastic/hastic-server/blob/master/server/src/models/analytic_unit.ts
import { AnalyticService } from '../services/analytic_service'
import {
AnalyticUnitId, AnalyticUnit,
AnalyticUnitsSet, AnalyticSegment, AnalyticSegmentsSearcher, AnalyticSegmentPair,
LabelingMode
} from '../models/analytic_unit';
import { MetricExpanded } from '../models/metric';
import { DatasourceRequest } from '../models/datasource';
import { Segment, SegmentId } from '../models/segment';
import { SegmentsSet } from '../models/segment_set';
import { SegmentArray } from '../models/segment_array';
import { HasticServerInfo, HasticServerInfoUnknown } from '../models/hastic_server_info';
import { Threshold, Condition } from '../models/threshold';
import text from '../partials/help_section.html';
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 class AnalyticController {
private _analyticUnitsSet: AnalyticUnitsSet;
private _selectedAnalyticUnitId: AnalyticUnitId = null;
private _labelingDataAddedSegments: SegmentsSet<AnalyticSegment>;
private _labelingDataRemovedSegments: SegmentsSet<AnalyticSegment>;
private _newAnalyticUnit: AnalyticUnit = null;
private _creatingNewAnalyticType: boolean = false;
private _savingNewAnalyticUnit: boolean = false;
private _tempIdCounted: number = -1;
private _graphLocked: boolean = false;
private _statusRunners: Set<AnalyticUnitId> = new Set<AnalyticUnitId>();
private _serverInfo: HasticServerInfo;
private _currentMetric: MetricExpanded;
private _currentDatasource: DatasourceRequest;
private _thresholds: Threshold[];
constructor(
private _grafanaUrl: string,
private _panelId: string,
private _panelObject: any,
private _emitter: Emitter,
private _analyticService?: AnalyticService,
) {
this._labelingDataAddedSegments = new SegmentArray<AnalyticSegment>();
this._labelingDataRemovedSegments = new SegmentArray<AnalyticSegment>();
this._analyticUnitsSet = new AnalyticUnitsSet([]);
this.fetchAnalyticUnits();
this._thresholds = [];
this.updateThresholds();
}
get helpSectionText() { return text; }
getSegmentsSearcher(): AnalyticSegmentsSearcher {
return this._segmentsSearcher.bind(this);
}
private _segmentsSearcher(point: number, rangeDist: number): AnalyticSegmentPair[] {
var result: AnalyticSegmentPair[] = [];
this._analyticUnitsSet.items.forEach(at => {
var segs = at.segments.findSegments(point, rangeDist);
segs.forEach(s => {
result.push({ analyticUnit: at, segment: s });
})
})
return result;
}
async sendThresholdParamsToServer(id: AnalyticUnitId) {
await this.saveThreshold(id);
await this._analyticService.runDetect(id);
await this._runStatusWaiter(this._analyticUnitsSet.byId(id));
}
createNew() {
this._newAnalyticUnit = new AnalyticUnit();
this._creatingNewAnalyticType = true;
this._savingNewAnalyticUnit = false;
if (this.analyticUnits.length === 0) {
this._newAnalyticUnit.labeledColor = ANALYTIC_UNIT_COLORS[0];
} else {
let colorIndex = ANALYTIC_UNIT_COLORS.indexOf(_.last(this.analyticUnits).labeledColor) + 1;
colorIndex %= ANALYTIC_UNIT_COLORS.length;
this._newAnalyticUnit.labeledColor = ANALYTIC_UNIT_COLORS[colorIndex];
}
}
async saveNew(metric: MetricExpanded, datasource: DatasourceRequest) {
this._savingNewAnalyticUnit = true;
this._newAnalyticUnit.id = await this._analyticService.postNewItem(
this._newAnalyticUnit, metric, datasource, this._grafanaUrl, this._panelId
);
if(this._newAnalyticUnit.detectorType === 'threshold') {
await this.saveThreshold(this._newAnalyticUnit.id);
}
this._analyticUnitsSet.addItem(this._newAnalyticUnit);
this._creatingNewAnalyticType = false;
this._savingNewAnalyticUnit = false;
if(this._newAnalyticUnit.detectorType !== 'threshold') {
this._runStatusWaiter(this._newAnalyticUnit);
}
}
get creatingNew() { return this._creatingNewAnalyticType; }
get saving() { return this._savingNewAnalyticUnit; }
get newAnalyticUnit(): AnalyticUnit { return this._newAnalyticUnit; }
get graphLocked() { return this._graphLocked; }
set graphLocked(value) { this._graphLocked = value; }
get labelingUnit(): AnalyticUnit {
if(this._selectedAnalyticUnitId === null) {
return null;
}
return this._analyticUnitsSet.byId(this._selectedAnalyticUnitId);
}
async toggleUnitTypeLabelingMode(id: AnalyticUnitId, metric: MetricExpanded, datasource: DatasourceRequest) {
this._currentMetric = metric;
this._currentDatasource = datasource;
if(this.labelingUnit && this.labelingUnit.saving) {
throw new Error('Can`t toggle during saving');
}
if(this._selectedAnalyticUnitId === id) {
return this.disableLabeling();
}
await this.disableLabeling();
this._selectedAnalyticUnitId = id;
this.labelingUnit.selected = true;
this.toggleLabelingMode(LabelingMode.LABELING);
this.toggleVisibility(id, true);
}
async disableLabeling() {
if(this._selectedAnalyticUnitId === null) {
return;
}
this.labelingUnit.saving = true;
const newIds = await this._saveLabelingData();
this._labelingDataAddedSegments.getSegments().forEach((s, i) => {
this.labelingUnit.segments.updateId(s.id, newIds[i]);
});
this.labelingUnit.saving = false;
let unit = this.labelingUnit;
this.dropLabeling();
this._runStatusWaiter(unit);
}
undoLabeling() {
this._labelingDataAddedSegments.getSegments().forEach(s => {
this.labelingUnit.segments.remove(s.id);
});
this._labelingDataRemovedSegments.getSegments().forEach(s => {
this.labelingUnit.segments.addSegment(s);
});
this.dropLabeling();
}
dropLabeling() {
this._labelingDataAddedSegments.clear();
this._labelingDataRemovedSegments.clear();
this.labelingUnit.selected = false;
this._selectedAnalyticUnitId = null;
this._tempIdCounted = -1;
}
get inLabelingMode(): boolean {
return this._selectedAnalyticUnitId !== null;
}
get labelingMode(): LabelingMode {
if(!this.inLabelingMode) {
return LabelingMode.NOT_IN_LABELING_MODE;
}
return this.labelingUnit.labelingMode;
}
set labelingMode(labelingMode: LabelingMode) {
this.labelingUnit.labelingMode = labelingMode;
}
addLabelSegment(segment: Segment, deleted = false) {
const asegment = this.labelingUnit.addLabeledSegment(segment, deleted);
this._labelingDataAddedSegments.addSegment(asegment);
}
get analyticUnits(): AnalyticUnit[] {
return this._analyticUnitsSet.items;
}
async onAnalyticUnitColorChange(id: AnalyticUnitId, value: string, deleted: boolean) {
if(id === undefined) {
throw new Error('id is undefined');
}
const analyticUnit = this._analyticUnitsSet.byId(id);
if(deleted) {
analyticUnit.deletedColor = value;
} else {
analyticUnit.labeledColor = value;
}
await this.saveAnalyticUnit(analyticUnit);
}
fetchAnalyticUnitsStatuses() {
this.analyticUnits.forEach(a => this._runStatusWaiter(a));
}
async fetchAnalyticUnitsSegments(from: number, to: number): Promise<void[]> {
if(!_.isNumber(+from)) {
throw new Error('from isn`t number');
}
if(!_.isNumber(+to)) {
throw new Error('to isn`t number');
}
var tasks = this.analyticUnits.map(a => this.fetchSegments(a, from, to));
return Promise.all(tasks);
}
async fetchSegments(analyticUnit: AnalyticUnit, from: number, to: number): Promise<void> {
if(!_.isNumber(+from)) {
throw new Error('from isn`t number');
}
if(!_.isNumber(+to)) {
throw new Error('to isn`t number');
}
var allSegmentsList = await this._analyticService.getSegments(analyticUnit.id, from, to);
var allSegmentsSet = new SegmentArray(allSegmentsList);
if(analyticUnit.selected) {
this._labelingDataAddedSegments.getSegments().forEach(s => allSegmentsSet.addSegment(s));
this._labelingDataRemovedSegments.getSegments().forEach(s => allSegmentsSet.remove(s.id));
}
analyticUnit.segments = allSegmentsSet;
}
private async _saveLabelingData(): Promise<SegmentId[]> {
let unit = this.labelingUnit;
if(unit === null) {
throw new Error('analytic unit is not selected');
}
if(
this._labelingDataAddedSegments.length === 0 &&
this._labelingDataRemovedSegments.length === 0
) {
return [];
}
await this._analyticService.updateMetric(unit.id, this._currentMetric, this._currentDatasource);
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;
}
redetectAll() {
this.analyticUnits.forEach(a => {
a.segments.clear();
this._runStatusWaiter(a);
this._analyticService.runDetect(a.id);
});
}
// TODO: move to renderer
updateFlotEvents(isEditMode: boolean, options: any): void {
if(options.grid.markings === undefined) {
options.markings = [];
}
for(var i = 0; i < this.analyticUnits.length; i++) {
const analyticUnit = this.analyticUnits[i];
if(!analyticUnit.visible) {
continue;
}
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, 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, 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 = defaultBorderColor;
let segmentFillColor = defaultFillColor;
if(s.deleted) {
segmentBorderColor = deletedSegmentBorderColor;
segmentFillColor = deletedSegmentFillColor;
} else {
if(s.labeled) {
segmentBorderColor = labeledSegmentBorderColor;
}
}
const expanded = s.expandDist(rangeDist, 0.01);
options.grid.markings.push({
xaxis: { from: expanded.from, to: expanded.to },
color: segmentFillColor
});
options.grid.markings.push({
xaxis: { from: expanded.from, to: expanded.from },
color: segmentBorderColor
});
options.grid.markings.push({
xaxis: { from: expanded.to, to: expanded.to },
color: segmentBorderColor
});
});
}
}
deleteLabelingAnalyticUnitSegmentsInRange(from: number, to: number): void {
const allRemovedSegs = this.labelingUnit.removeSegmentsInRange(from, to);
allRemovedSegs.forEach(s => {
if(!this._labelingDataAddedSegments.has(s.id)) {
this._labelingDataRemovedSegments.addSegment(s);
}
});
this._labelingDataAddedSegments.removeInRange(from, to);
}
toggleLabelingMode(labelingMode: LabelingMode): void {
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;
}
}
async removeAnalyticUnit(id: AnalyticUnitId, silent: boolean = false): Promise<void> {
if(id === this._selectedAnalyticUnitId) {
this.dropLabeling();
}
if(!silent) {
await this._analyticService.removeAnalyticUnit(id);
}
this._analyticUnitsSet.removeItem(id);
}
async toggleAnalyticUnitAlert(analyticUnit: AnalyticUnit): Promise<void> {
analyticUnit.alert = analyticUnit.alert ? true : false;
// TODO: saveAnalyticUnit instead of specific method
await this._analyticService.setAnalyticUnitAlert(analyticUnit);
}
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);
analyticUnit.saving = false;
}
async getAnalyticUnits(): Promise<any[]> {
if(this._analyticService === undefined) {
return [];
}
return this._analyticService.getAnalyticUnits(this._panelId);
}
async fetchAnalyticUnits(): Promise<void> {
const units = await this.getAnalyticUnits();
this._analyticUnitsSet = new AnalyticUnitsSet(units);
}
async updateThresholds(): Promise<void> {
if(this._analyticService === undefined) {
return;
}
const ids = _.map(this._panelObject.analyticUnits, (analyticUnit: any) => analyticUnit.id);
const thresholds = await this._analyticService.getThresholds(ids);
this._thresholds = thresholds;
}
getThreshold(id: AnalyticUnitId): Threshold {
let threshold = _.find(this._thresholds, { id });
if(threshold === undefined) {
threshold = {
id,
value: 0,
condition: Condition.ABOVE
};
this._thresholds.push(threshold);
}
return threshold;
}
async saveThreshold(id: AnalyticUnitId) {
const threshold = this.getThreshold(id);
if(threshold.value === undefined) {
throw new Error('Cannot save threshold with undefined value');
}
if(threshold.condition === undefined) {
throw new Error('Cannot save threshold with undefined condition');
}
return this._analyticService.updateThreshold(threshold);
}
public get conditions() {
return _.values(Condition);
}
private async _runStatusWaiter(analyticUnit: AnalyticUnit) {
if(this._analyticService === undefined) {
return;
}
if(analyticUnit === undefined || analyticUnit === null) {
throw new Error('analyticUnit not defined');
}
if(analyticUnit.id === undefined) {
throw new Error('analyticUnit.id is undefined');
}
if(this._statusRunners.has(analyticUnit.id)) {
return;
}
this._statusRunners.add(analyticUnit.id);
var statusGenerator = this._analyticService.getStatusGenerator(
analyticUnit.id, 1000
);
for await (const data of statusGenerator) {
if(data === undefined) {
break;
}
let status = data.status;
let error = data.errorMessage;
if(analyticUnit.status !== status) {
analyticUnit.status = status;
if(error !== undefined) {
analyticUnit.error = error;
}
this._emitter.emit('analytic-unit-status-change', analyticUnit);
}
if(!analyticUnit.isActiveStatus) {
break;
}
}
this._statusRunners.delete(analyticUnit.id);
}
public getNewTempSegmentId(): SegmentId {
this._tempIdCounted--;
return this._tempIdCounted.toString();
}
public async 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);
}
public onAnalyticUnitDetectorChange(analyticUnitTypes: any) {
this.newAnalyticUnit.type = analyticUnitTypes[this.newAnalyticUnit.detectorType][0].value;
}
public async updateServerInfo() {
if(!this._analyticService) {
this._serverInfo = HasticServerInfoUnknown;
return;
}
this._serverInfo = await this._analyticService.getServerInfo();
}
public get serverInfo() {
return this._serverInfo;
}
public get serverStatus(): boolean {
if(this._analyticService === undefined) {
return false;
}
return this._analyticService.isUp;
}
}
function addAlphaToRGB(colorString: string, alpha: number): string {
let color = tinycolor(colorString);
if (color.isValid()) {
color.setAlpha(color.getAlpha() * alpha);
return color.toRgbString();
} else {
return colorString;
}
}