From f413f5369d202e7e92510d777d31664fdd1c1098 Mon Sep 17 00:00:00 2001 From: vargburz Date: Wed, 18 May 2022 14:01:44 +0300 Subject: [PATCH 1/4] fill defaults && init bar pod --- package.json | 5 +-- src/components/Panel.tsx | 46 +++++++++++++++++++++++----- yarn.lock | 66 ++++++++++++++++++++++++++++++++++------ 3 files changed, 98 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 8c56f20..365a2c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grafana-chartwerk-panel", - "version": "0.4.0", + "version": "0.4.1", "description": "Chartwerk Panel", "scripts": { "build": "grafana-toolkit plugin:build", @@ -14,7 +14,8 @@ "author": "CorpGlory Inc.", "license": "GPL V3", "devDependencies": { - "@chartwerk/gauge-pod": "0.4.1", + "@chartwerk/gauge-pod": "0.5.0", + "@chartwerk/bar-pod": "0.5.0", "@grafana/data": "latest", "@grafana/toolkit": "latest", "@grafana/ui": "latest", diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 50213a2..c0070cb 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -1,11 +1,12 @@ import { Options } from '../models/options'; import { Series } from '../models/series'; -import { PanelOptions } from '../types'; +import { PanelOptions, Pod } from '../types'; import { DataProcessor } from '../grafana/data_processor'; import { ChartwerkGaugePod } from '@chartwerk/gauge-pod'; +import { ChartwerkBarPod } from '@chartwerk/bar-pod'; import { PanelData, TimeRange, PanelProps } from '@grafana/data'; @@ -16,27 +17,37 @@ import * as _ from 'lodash'; interface Props extends PanelProps {} export function Panel({ options, data, width, height, timeZone, timeRange, onChangeTimeRange }: Props) { - console.log('options', options); + const panelOptions = fillGrafanaOptionsWithDefaults(options); + console.log('panelOptions', panelOptions); const grafanaSeriesList = getGrafanaSeriesList(data, timeRange); - const series = new Series(grafanaSeriesList, options.gauge.value).getChartwerkSeries(); + const series = new Series(grafanaSeriesList, panelOptions.gauge.value).getChartwerkSeries(); console.log('series', series); - const chartwerkOptions = new Options(grafanaSeriesList, options).getChartwerkOptions(); + const chartwerkOptions = new Options(grafanaSeriesList, panelOptions).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(() => { - // TODO: switch / case pod type - const pod = new ChartwerkGaugePod((chartContainer as any).current, series, chartwerkOptions); + let pod; + switch (panelOptions.visualizationType) { + case Pod.GAUGE: + pod = new ChartwerkGaugePod((chartContainer as any).current, series, chartwerkOptions); + break; + case Pod.BAR: + pod = new ChartwerkBarPod((chartContainer as any).current, series, chartwerkOptions); + break; + default: + throw new Error(`Unknown visualization type: ${panelOptions.visualizationType}`); + } pod.render(); }); - const isLinkActive = !_.isEmpty(options.gauge.link); + const isLinkActive = !_.isEmpty(panelOptions.gauge.link); const chartClickHandler = (event: React.MouseEvent) => { event.preventDefault(); if (!isLinkActive) { return; } - window.open(options.gauge.link, '_self'); + window.open(panelOptions.gauge.link, '_self'); }; return ( @@ -59,3 +70,22 @@ function getGrafanaSeriesList(grafanaData: PanelData, timeRange: TimeRange): any range: timeRange, }); } + +function fillGrafanaOptionsWithDefaults(options: PanelOptions): PanelOptions { + const defaults = { + gauge: { + min: {}, + max: {}, + value: {}, + valueSize: 20, + reversed: false, + thresholds: { + arcBackground: 'gray', + defaultColor: 'green', + thresholds: [], + }, + icons: [], + }, + }; + return _.defaultsDeep(options, defaults); +} diff --git a/yarn.lock b/yarn.lock index acc72c5..d3c38f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -902,16 +902,27 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz#fe364f025ba74f6de6c837a84ef44bdb1d61e68f" integrity sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w== -"@chartwerk/core@github:chartwerk/core#a30ca83842247c79969deaaacfc7fb444a60cefb": - version "0.1.0" - resolved "https://codeload.github.com/chartwerk/core/tar.gz/a30ca83842247c79969deaaacfc7fb444a60cefb" +"@chartwerk/bar-pod@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@chartwerk/bar-pod/-/bar-pod-0.5.0.tgz#8550800fa33f2ea49285a3a3a36c04a802d14d9b" + integrity sha512-qZIq0Eq5VDhtcrKusL/gKRRNr4g1tDIRQ0uZd6hGG8LYeT8s5AUG1tYZNNrROutMTKvvaSg56XoQKBY9xvZZaA== + dependencies: + "@chartwerk/core" "^0.5.0" -"@chartwerk/gauge-pod@0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@chartwerk/gauge-pod/-/gauge-pod-0.4.1.tgz#ac346d777f72ec855e51f5f7c8c01e12a1e1cb5c" - integrity sha512-Ik6Dr4AJP/L+7YjZVJ9W19ujXXB5/b5A3Qxboi491hrXYlqMrfAx/LoyfDAtSEmGNhK5qpT8XLluzuHVcgTY4g== +"@chartwerk/core@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@chartwerk/core/-/core-0.5.0.tgz#7d641a5ee3ec9ca588f5b06a0504659113745636" + integrity sha512-YFqBJ8WFb83yZO2VR+XVRSMX3+ErGcGcdHy5aLeLzOqBW09gO7NENdzlCWXQLsGSx+f9RK2GcSTM4z0Q7OEDfA== dependencies: - "@chartwerk/core" "github:chartwerk/core#a30ca83842247c79969deaaacfc7fb444a60cefb" + d3 "^5.7.2" + lodash "^4.14.149" + +"@chartwerk/gauge-pod@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@chartwerk/gauge-pod/-/gauge-pod-0.5.0.tgz#4bf1022b5ae3b9536ef3a5bcb0f872fbd9c685d4" + integrity sha512-x+MK737RB8h3c42GJUZoGDKfOcdtwCP58KLdxctzVzH9b0oHdnEGO+BE5M/EwPuoSq1H5F4DqBg8NCLfIVTlvw== + dependencies: + "@chartwerk/core" "^0.5.0" "@cnakazawa/watch@^1.0.3": version "1.0.4" @@ -4811,6 +4822,43 @@ d3@5.15.0: d3-voronoi "1" d3-zoom "1" +d3@^5.7.2: + version "5.16.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877" + integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw== + dependencies: + d3-array "1" + d3-axis "1" + d3-brush "1" + d3-chord "1" + d3-collection "1" + d3-color "1" + d3-contour "1" + d3-dispatch "1" + d3-drag "1" + d3-dsv "1" + d3-ease "1" + d3-fetch "1" + d3-force "1" + d3-format "1" + d3-geo "1" + d3-hierarchy "1" + d3-interpolate "1" + d3-path "1" + d3-polygon "1" + d3-quadtree "1" + d3-random "1" + d3-scale "2" + d3-scale-chromatic "1" + d3-selection "1" + d3-shape "1" + d3-time "1" + d3-time-format "2" + d3-timer "1" + d3-transition "1" + d3-voronoi "1" + d3-zoom "1" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -8030,7 +8078,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.21, lodash@^4.1.1, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.1.1, lodash@^4.14.149, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== From 81bfb28b1f1fd6991adbd7c3cdd992345bac3bcb Mon Sep 17 00:00:00 2001 From: vargburz Date: Wed, 18 May 2022 14:15:50 +0300 Subject: [PATCH 2/4] fix extremumns --- src/components/Panel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index c0070cb..16a7b32 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -74,9 +74,9 @@ function getGrafanaSeriesList(grafanaData: PanelData, timeRange: TimeRange): any function fillGrafanaOptionsWithDefaults(options: PanelOptions): PanelOptions { const defaults = { gauge: { - min: {}, - max: {}, - value: {}, + min: { useMetric: false }, + max: { useMetric: false }, + value: { useMetric: false }, valueSize: 20, reversed: false, thresholds: { From 9339ef653bbb8a560316107cff0a0d6e11ff8382 Mon Sep 17 00:00:00 2001 From: vargburz Date: Wed, 18 May 2022 14:39:31 +0300 Subject: [PATCH 3/4] remove defaults --- src/components/Panel.tsx | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 16a7b32..5de391e 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -17,18 +17,17 @@ import * as _ from 'lodash'; interface Props extends PanelProps {} export function Panel({ options, data, width, height, timeZone, timeRange, onChangeTimeRange }: Props) { - const panelOptions = fillGrafanaOptionsWithDefaults(options); - console.log('panelOptions', panelOptions); + console.log('panelOptions', options); const grafanaSeriesList = getGrafanaSeriesList(data, timeRange); - const series = new Series(grafanaSeriesList, panelOptions.gauge.value).getChartwerkSeries(); + const series = new Series(grafanaSeriesList, options.gauge.value).getChartwerkSeries(); console.log('series', series); - const chartwerkOptions = new Options(grafanaSeriesList, panelOptions).getChartwerkOptions(); + 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(() => { let pod; - switch (panelOptions.visualizationType) { + switch (options.visualizationType) { case Pod.GAUGE: pod = new ChartwerkGaugePod((chartContainer as any).current, series, chartwerkOptions); break; @@ -36,18 +35,18 @@ export function Panel({ options, data, width, height, timeZone, timeRange, onCha pod = new ChartwerkBarPod((chartContainer as any).current, series, chartwerkOptions); break; default: - throw new Error(`Unknown visualization type: ${panelOptions.visualizationType}`); + throw new Error(`Unknown visualization type: ${options.visualizationType}`); } pod.render(); }); - const isLinkActive = !_.isEmpty(panelOptions.gauge.link); + const isLinkActive = !_.isEmpty(options.gauge.link); const chartClickHandler = (event: React.MouseEvent) => { event.preventDefault(); if (!isLinkActive) { return; } - window.open(panelOptions.gauge.link, '_self'); + window.open(options.gauge.link, '_self'); }; return ( @@ -70,22 +69,3 @@ function getGrafanaSeriesList(grafanaData: PanelData, timeRange: TimeRange): any range: timeRange, }); } - -function fillGrafanaOptionsWithDefaults(options: PanelOptions): PanelOptions { - const defaults = { - gauge: { - min: { useMetric: false }, - max: { useMetric: false }, - value: { useMetric: false }, - valueSize: 20, - reversed: false, - thresholds: { - arcBackground: 'gray', - defaultColor: 'green', - thresholds: [], - }, - icons: [], - }, - }; - return _.defaultsDeep(options, defaults); -} From ba70b6faa69d4e5cd90fdafeb0b423b3a77042e1 Mon Sep 17 00:00:00 2001 From: vargburz Date: Wed, 18 May 2022 16:25:37 +0300 Subject: [PATCH 4/4] bar pod init --- src/components/Panel.tsx | 17 +- src/models/barSeries.ts | 25 +++ src/models/options/barOptions.ts | 155 ++++++++++++++++++ .../{options.ts => options/gaugeOptions.ts} | 4 +- 4 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 src/models/barSeries.ts create mode 100644 src/models/options/barOptions.ts rename src/models/{options.ts => options/gaugeOptions.ts} (99%) 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 }> = [];