diff --git a/src/components/editors/IconsEditor.tsx b/src/components/editors/IconsEditor.tsx index 9aee9cd..0617345 100644 --- a/src/components/editors/IconsEditor.tsx +++ b/src/components/editors/IconsEditor.tsx @@ -1,4 +1,4 @@ -import { IconPosition } from 'types'; +import { IconPosition, Icon, Condition } from 'types'; import { GrafanaTheme, SelectableValue, StandardEditorProps } from '@grafana/data'; import { @@ -18,23 +18,6 @@ import React from 'react'; import * as _ from 'lodash'; -type IconConfig = { - position: IconPosition; - url: string; - metrics: string[]; - conditions: Condition[]; - values: number[]; - size: number; -}; - -enum Condition { - EQUAL = '=', - GREATER = '>', - LESS = '<', - GREATER_OR_EQUAL = '>=', - LESS_OR_EQUAL = '<=', -} - const positionOptions: Array> = [ { label: 'Upper-Left', @@ -73,7 +56,7 @@ const conditionOptions: Array> = [ }, ]; -const DEFAULT_ICON: IconConfig = { +const DEFAULT_ICON: Icon = { position: IconPosition.UPPER_LEFT, url: '', size: 40, @@ -86,7 +69,7 @@ const fieldNamePickerSettings = { settings: { width: 24 }, } as any; -export function IconsEditor({ onChange, value, context }: StandardEditorProps) { +export function IconsEditor({ onChange, value, context }: StandardEditorProps) { const icons = value; const addIcon = () => { @@ -113,14 +96,14 @@ export function IconsEditor({ onChange, value, context }: StandardEditorProps { + const onIconFieldChange = (iconIdx: number, field: keyof Icon, value: any) => { // @ts-ignore icons[iconIdx][field] = value; onChange(icons); }; - const onConditionChange = (iconIdx: number, conditionIdx: number, field: keyof IconConfig, value: any) => { + const onConditionChange = (iconIdx: number, conditionIdx: number, field: keyof Icon, value: any) => { // @ts-ignore icons[iconIdx][field][conditionIdx] = value; diff --git a/src/models/options.ts b/src/models/options.ts index 828fb6b..642250f 100644 --- a/src/models/options.ts +++ b/src/models/options.ts @@ -1,4 +1,4 @@ -import { PanelOptions, Aggregation, Threshold } from 'types'; +import { PanelOptions, Aggregation, Threshold, Icon, IconPosition, Condition } from 'types'; import { filterMetricListByAlias, getAggregatedValueFromSerie } from '../utils'; @@ -13,11 +13,13 @@ export class Options { private minValue: number | undefined; private maxValue: number | undefined; private thresholds: { value: number, color: string }[] = []; + private icons: { src: string, position: string, size: number}[] = []; constructor(private grafanaSeriesList: any[], private grafanaOptions: PanelOptions) { this._setMin(); this._setMax(); this._setThresholds(); + this._setIcons(); } private _setMin(): void { @@ -65,8 +67,81 @@ export class Options { return `${value.toFixed(decimals)} ${suffix}`; } + private _setIcons(): void { + if (_.isEmpty(this.grafanaOptions.gauge.icons)) { + return; + } + for (let [idx, icon] of this.grafanaOptions.gauge.icons.entries()) { + this._setIcon(icon, idx); + } + } + + private _setIcon(icon: Icon, idx: number): void { + if (!this._areIconConditionsFulfilled(icon, idx)) { + return; + } + this.icons.push({ + src: icon.url, + size: icon.size, + position: this._getChartwerkIconPosition(icon.position), + }); + } + + private _areIconConditionsFulfilled(icon: Icon, iconIdx: number): boolean { + if(_.isEmpty(icon.metrics)) { + return true; + } + + // check each condition and return false if something goes wrong + for (let [conditionIdx, metric] of icon.metrics.entries()) { + const value = this.getLastValueFromMetrics(metric, `Icon ${iconIdx + 1}, Condition ${conditionIdx}`); + if(value === null || value === undefined) { + // TODO: may be throw an error + return false; + } + if (!this.checkIconCondition(value, icon.values[conditionIdx], icon.conditions[conditionIdx])) { + return false; + } + } + + return true; + } + + private checkIconCondition(metricValue: number, inputValue: number, condition: Condition): boolean { + if (inputValue === undefined || inputValue === null) { + return true; + } + switch (condition) { + case Condition.EQUAL: + return metricValue === inputValue; + case Condition.GREATER: + return metricValue > inputValue; + case Condition.GREATER_OR_EQUAL: + return metricValue >= inputValue; + case Condition.LESS: + return metricValue < inputValue; + case Condition.LESS_OR_EQUAL: + return metricValue <= inputValue; + default: + throw new Error(`Unknown condition: ${condition}`); + } + } + + private _getChartwerkIconPosition(position: IconPosition): string { + // TODO: use chartwerk types + switch (position) { + case IconPosition.MIDDLE: + return 'middle'; + case IconPosition.UPPER_LEFT: + return 'left'; + case IconPosition.UPPER_RIGHT: + return 'right'; + default: + throw new Error(`Unknown Icon Position ${position}`); + } + } + getChartwerkOptions(): any { - console.log('opt', this.maxValue, this.minValue); return { maxValue: this.maxValue, minValue: this.minValue, @@ -76,12 +151,12 @@ export class Options { reversed: this.grafanaOptions.gauge.reversed, stops: this.thresholds, valueFontSize: this.grafanaOptions.gauge.valueSize, - // @ts-ignore - icons: [{ src: 'https://cityhost.ua/upload_img/blog5ef308ea5529c_trash2-01.jpg', position: 'middle', size: 30 }], + icons: this.icons, }; } getLastValueFromMetrics(metricName: string | undefined, optionName: string): number | null { + // optionName -> helper in Error, mb use option path instead const filteredSeries = filterMetricListByAlias( this.grafanaSeriesList, metricName, diff --git a/src/models/series.ts b/src/models/series.ts index b1956a9..a2b7b29 100644 --- a/src/models/series.ts +++ b/src/models/series.ts @@ -29,7 +29,7 @@ export class Series { return { target: serie.alias, color: serie.color, - datapoints: _.map(serie.datapoints, (row) => _.reverse(row)), + datapoints: _.map(serie.datapoints, (row) => _.reverse(_.clone(row))), alias: serie.label, }; }); diff --git a/src/types.ts b/src/types.ts index 88dfaec..a0a2e94 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,7 +12,8 @@ export interface PanelOptions { arcBackground: string; defaultColor: string; thresholds: Threshold[]; - } + }; + icons: Icon[]; }; } @@ -50,3 +51,20 @@ export type Threshold = { metricName: string; color: string; } + +export type Icon = { + conditions: Condition[]; + metrics: string[]; + values: number[]; + position: IconPosition; + size: number; + url: string; +}; + +export enum Condition { + EQUAL = '=', + GREATER = '>', + LESS = '<', + GREATER_OR_EQUAL = '>=', + LESS_OR_EQUAL = '<=', +} diff --git a/src/utils.ts b/src/utils.ts index dff1836..014f99e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -13,8 +13,9 @@ export function filterMetricListByAlias(list: any[], alias: string | undefined, return filteredSeries; } -export function getAggregatedValueFromSerie(serie: any, aggregation = Aggregation.LAST): number | null { +export function getAggregatedValueFromSerie(serie: any, aggregation = Aggregation.LAST, valueIdx: 0 | 1 = 0): number | null { // series types { datapoints: [number, number][]} + // valueIdx === 0 for Grafana series, valueIdx === 1 for Chartwerk series if (serie === undefined) { return null; } @@ -24,9 +25,8 @@ export function getAggregatedValueFromSerie(serie: any, aggregation = Aggregatio switch (aggregation) { case Aggregation.LAST: const lastRow = _.last(serie.datapoints as Array<[number, number]>); - // [0] because it is Grafan series. So 0 idx for values, 1 idx for timestamps // @ts-ignore - return !_.isEmpty(lastRow) ? lastRow[0] : null; + return !_.isEmpty(lastRow) ? lastRow[valueIdx] : null; default: throw new Error(`Unknown aggregation type: ${aggregation}`); }