diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 5de391e..8d98fff 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -1,5 +1,7 @@ -import { Options } from '../models/options'; +import { GaugeOptions } from '../models/options/gaugeOptions'; +import { BarOptions } from '../models/options/barOptions'; import { Series } from '../models/series'; +import { BarSeries } from '../models/barSeries'; import { PanelOptions, Pod } from '../types'; @@ -19,20 +21,23 @@ interface Props extends PanelProps {} export function Panel({ options, data, width, height, timeZone, timeRange, onChangeTimeRange }: Props) { console.log('panelOptions', options); const grafanaSeriesList = getGrafanaSeriesList(data, timeRange); - const series = new Series(grafanaSeriesList, options.gauge.value).getChartwerkSeries(); - console.log('series', series); - const chartwerkOptions = new Options(grafanaSeriesList, options).getChartwerkOptions(); let chartContainer = useRef(null); // we request animation frame here because we need an existing DOM-element at the moment we render the pod window.requestAnimationFrame(() => { + console.log('pod', options.visualizationType); let pod; switch (options.visualizationType) { case Pod.GAUGE: - pod = new ChartwerkGaugePod((chartContainer as any).current, series, chartwerkOptions); + const series = new Series(grafanaSeriesList, options.gauge.value).getChartwerkSeries(); + const chartwerkGaugeOptions = new GaugeOptions(grafanaSeriesList, options).getChartwerkOptions(); + pod = new ChartwerkGaugePod((chartContainer as any).current, series, chartwerkGaugeOptions); break; case Pod.BAR: - pod = new ChartwerkBarPod((chartContainer as any).current, series, chartwerkOptions); + const barSeries = new BarSeries(grafanaSeriesList).getChartwerkSeries(); + const chartwerkBarOptions = new BarOptions(grafanaSeriesList, options).getChartwerkOptions(); + console.log('data', barSeries, chartwerkBarOptions); + pod = new ChartwerkBarPod((chartContainer as any).current, barSeries, chartwerkBarOptions); break; default: throw new Error(`Unknown visualization type: ${options.visualizationType}`); diff --git a/src/models/barSeries.ts b/src/models/barSeries.ts new file mode 100644 index 0000000..9c3cb01 --- /dev/null +++ b/src/models/barSeries.ts @@ -0,0 +1,25 @@ +import * as _ from 'lodash'; + +// Convert Grafana series into Chartwerk series +export class BarSeries { + private _seriesList; + + constructor(grafanaSeriesList: any) { + this._seriesList = this._updateSeriesListWithChartwerkParams(grafanaSeriesList); + } + + getChartwerkSeries(): any[] { + return this._seriesList; + } + + private _updateSeriesListWithChartwerkParams(series: any[]): any[] { + return _.map(series, (serie: any, idx: number) => { + return { + target: serie.alias, + color: serie.color, + datapoints: _.map(serie.datapoints, (row) => _.reverse(_.clone(row))), + alias: serie.label, + }; + }); + } +} diff --git a/src/models/options/barOptions.ts b/src/models/options/barOptions.ts new file mode 100644 index 0000000..4966453 --- /dev/null +++ b/src/models/options/barOptions.ts @@ -0,0 +1,155 @@ +import { PanelOptions, Aggregation, Threshold, Icon, IconPosition, Condition } from 'types'; + +import { filterMetricListByAlias, getAggregatedValueFromSerie } from '../../utils'; + +import _ from 'lodash'; + +// Convert Grafana options into Chartwerk Bar options +export class BarOptions { + private thresholds: Array<{ value: number; color: string }> = []; + private icons: Array<{ src: string; position: string; size: number }> = []; + + constructor(private grafanaSeriesList: any[], private grafanaOptions: PanelOptions) { + this._setThresholds(); + this._setIcons(); + } + + private _setThresholds(): void { + if (_.isEmpty(this.grafanaOptions.gauge.thresholds.thresholds)) { + return; + } + for (let [idx, threshold] of this.grafanaOptions.gauge.thresholds.thresholds.entries()) { + this._setThreshold(threshold, idx); + } + } + + private _setThreshold(threshold: Threshold, idx: number): void { + const value = threshold.useMetric + ? this.getLastValueFromMetrics(threshold.metricName, `Threshold ${idx + 1}`) + : threshold.value; + if (value === null || value === undefined) { + // TODO: may be throw an error + return; + } + this.thresholds.push({ + value, + color: threshold.color, + }); + } + + 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 + 1}`); + 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 { + return { + axis: { + x: { + format: 'custom', + valueFormatter: (value: any) => { + return 'L' + value; + }, + }, + y: { + format: 'custom', + range: [-100, 100], + valueFormatter: (value: any) => { + return value + '%'; + }, + }, + }, + stacked: false, + matching: false, + zoomEvents: { + scroll: { zoom: { isActive: false }, pan: { isActive: false } }, + }, + annotations: [ + { key: 'm-1', color: 'red' }, + { key: 'm-2', color: 'green' }, + ], + eventsCallbacks: { + zoomIn: (range: any) => { + console.log('range', range); + }, + }, + }; + } + + getLastValueFromMetrics(metricName: string | undefined, optionName: string): number | null { + // optionName -> helper in Error, mb use option path instead + const filteredSeries = filterMetricListByAlias(this.grafanaSeriesList, metricName, optionName); + const serie = filteredSeries[0]; + // Last value for now + return getAggregatedValueFromSerie(serie, Aggregation.LAST); + } +} diff --git a/src/models/options.ts b/src/models/options/gaugeOptions.ts similarity index 99% rename from src/models/options.ts rename to src/models/options/gaugeOptions.ts index f150d29..6f7f4ec 100644 --- a/src/models/options.ts +++ b/src/models/options/gaugeOptions.ts @@ -1,13 +1,13 @@ import { PanelOptions, Aggregation, Threshold, Icon, IconPosition, Condition } from 'types'; -import { filterMetricListByAlias, getAggregatedValueFromSerie } from '../utils'; +import { filterMetricListByAlias, getAggregatedValueFromSerie } from '../../utils'; import { getValueFormat } from '@grafana/data'; import _ from 'lodash'; // Convert Grafana options into Chartwerk Gauge options -export class Options { +export class GaugeOptions { private minValue: number | undefined; private maxValue: number | undefined; private thresholds: Array<{ value: number; color: string }> = [];