From f413f5369d202e7e92510d777d31664fdd1c1098 Mon Sep 17 00:00:00 2001 From: vargburz Date: Wed, 18 May 2022 14:01:44 +0300 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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 }> = []; From 07bb1392f1e7e3a30f66467d8c2b72b1c6ad3e20 Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 15 Jul 2022 18:54:34 +0300 Subject: [PATCH 5/6] merge master --- README.md | 35 ++++++++++++++++++++-- package.json | 6 ++-- src/components/Panel.tsx | 2 -- src/components/editors/UseMetricEditor.tsx | 10 +++---- src/grafana/data_processor.ts | 13 ++------ src/models/options/gaugeOptions.ts | 10 +++++-- src/models/series.ts | 11 +++++-- src/module.ts | 3 +- src/plugin.json | 10 +++++-- yarn.lock | 28 ++++++++--------- 10 files changed, 81 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 8cb519f..8424396 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,35 @@ - Bar Chart (coming soon) - Gauge: dynamic thresholds and min / max - Gauge: conditional icons displaying +- Gauge: reversed direction + +## How to use + +1. Create a new panel and select Chartwerk as the visualization +2. Add queries with unique aliases +3. Go to the Options Tab and setup panel: + - Choose visualization type + - Select metric in the Value -> Metric dropdown (by default, the first metric is used) + +## Demo + +see [demo](https://grafana.corpglory.com/d/8vGyMypGz/demo-home?orgId=4) + +## Options [Gauge] + +- Visualization: + - Pod: option to select chart type +- Value: + - Metric: select metric query from dropdown +- Extemum: + - Min: + - type number for static minimum value OR + - enable "Use metric" toggle switch to select metric as minimun + - default value: 0 + - Max: + - type number for static maximum OR + - enable "Use metric" toggle switch to select metric as maximum + - default value: maximum of metric query ## Installation @@ -27,13 +56,13 @@ - Download Chartwerk panel ``` -wget https://gitlab.com/chartwerk/grafana-chartwerk-panel/uploads/117d957cd20276826cd092becb62dd30/corpglory-chartwerk-panel-0.4.0.zip +wget https://gitlab.com/chartwerk/grafana-chartwerk-panel/uploads/2284215a4dc8fb3bde1fd3b51bd99d3e/corpglory-chartwerk-panel-0.5.0.zip ``` - Unpack downloaded files ``` -unzip -u corpglory-chartwerk-panel-0.4.0.zip -d corpglory-chartwerk-panel +unzip -u corpglory-chartwerk-panel-0.4.1.zip -d corpglory-chartwerk-panel ``` - Restart grafana-server @@ -51,6 +80,6 @@ You can install Chartwerk panel to Grafana in Docker passing it as environment v ```bash docker run \ -p 3000:3000 \ - -e "GF_INSTALL_PLUGINS=https://gitlab.com/chartwerk/grafana-chartwerk-panel/uploads/117d957cd20276826cd092becb62dd30/corpglory-chartwerk-panel-0.4.0.zip;corpglory-chartwerk-panel" \ + -e "GF_INSTALL_PLUGINS=https://gitlab.com/chartwerk/grafana-chartwerk-panel/uploads/2284215a4dc8fb3bde1fd3b51bd99d3e/corpglory-chartwerk-panel-0.5.0.zip;corpglory-chartwerk-panel" \ grafana/grafana ``` diff --git a/package.json b/package.json index 365a2c0..5ad0aee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grafana-chartwerk-panel", - "version": "0.4.1", + "version": "0.5.0", "description": "Chartwerk Panel", "scripts": { "build": "grafana-toolkit plugin:build", @@ -14,8 +14,8 @@ "author": "CorpGlory Inc.", "license": "GPL V3", "devDependencies": { - "@chartwerk/gauge-pod": "0.5.0", - "@chartwerk/bar-pod": "0.5.0", + "@chartwerk/gauge-pod": "^0.6.2", + "@chartwerk/bar-pod": "^0.6.2", "@grafana/data": "latest", "@grafana/toolkit": "latest", "@grafana/ui": "latest", diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 8d98fff..481bf56 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -19,9 +19,7 @@ import * as _ from 'lodash'; interface Props extends PanelProps {} export function Panel({ options, data, width, height, timeZone, timeRange, onChangeTimeRange }: Props) { - console.log('panelOptions', options); const grafanaSeriesList = getGrafanaSeriesList(data, timeRange); - 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(() => { diff --git a/src/components/editors/UseMetricEditor.tsx b/src/components/editors/UseMetricEditor.tsx index 4018e26..d35b375 100644 --- a/src/components/editors/UseMetricEditor.tsx +++ b/src/components/editors/UseMetricEditor.tsx @@ -18,7 +18,7 @@ const fieldNamePickerSettings = { } as any; export function UseMetricEditor({ onChange, value, context }: StandardEditorProps) { - const config = value; + let config: UseMetricConfig = value; const onFieldChange = (field: keyof UseMetricConfig, value: any) => { // @ts-ignore @@ -31,22 +31,22 @@ export function UseMetricEditor({ onChange, value, context }: StandardEditorProp onFieldChange('useMetric', (evt.target as any).checked)} /> - {config.useMetric ? ( + {config?.useMetric ? ( onFieldChange('metricName', newVal)} item={fieldNamePickerSettings} /> ) : ( onFieldChange('value', (evt.target as any).value)} /> diff --git a/src/grafana/data_processor.ts b/src/grafana/data_processor.ts index 530bb6b..2be8ab0 100644 --- a/src/grafana/data_processor.ts +++ b/src/grafana/data_processor.ts @@ -2,16 +2,7 @@ import { applyNullInsertThreshold } from './null_insert'; import { find } from 'lodash'; -import { - DataFrame, - dateTime, - Field, - FieldType, - getColorForTheme, - getFieldDisplayName, - getTimeField, - TimeRange, -} from '@grafana/data'; +import { DataFrame, dateTime, Field, FieldType, getFieldDisplayName, getTimeField, TimeRange } from '@grafana/data'; import { colors } from '@grafana/ui'; import config from 'grafana/app/core/config'; import TimeSeries from 'grafana/app/core/time_series2'; @@ -91,7 +82,7 @@ export class DataProcessor { const series = new TimeSeries({ datapoints: datapoints || [], alias: alias, - color: getColorForTheme(color, config.theme), + color: config.theme.visualization.getColorByName(color), unit: field.config ? field.config.unit : undefined, dataFrameIndex, fieldIndex, diff --git a/src/models/options/gaugeOptions.ts b/src/models/options/gaugeOptions.ts index 6f7f4ec..5bc4698 100644 --- a/src/models/options/gaugeOptions.ts +++ b/src/models/options/gaugeOptions.ts @@ -21,7 +21,10 @@ export class GaugeOptions { } private _setMin(): void { - if (!this.grafanaOptions.gauge.min.useMetric) { + if (!this.grafanaOptions.gauge.min) { + throw new Error(`Min Config is not selected: [See options: Extremum -> Min]`); + } + if (!this.grafanaOptions.gauge.min?.useMetric) { this.minValue = this.grafanaOptions.gauge.min.value; return; } @@ -30,7 +33,10 @@ export class GaugeOptions { } private _setMax(): void { - if (!this.grafanaOptions.gauge.max.useMetric) { + if (!this.grafanaOptions.gauge.max) { + throw new Error(`Max Config is not selected: [See options: Extremum -> Max]`); + } + if (!this.grafanaOptions.gauge.max?.useMetric) { this.maxValue = this.grafanaOptions.gauge.max.value; return; } diff --git a/src/models/series.ts b/src/models/series.ts index 3155a82..d426763 100644 --- a/src/models/series.ts +++ b/src/models/series.ts @@ -9,9 +9,14 @@ export class Series { private _seriesList; private _selectedSerieName; - constructor(grafanaSeriesList: any, private gaugeValueOptions: ValueOptions) { - if (_.isEmpty(this.gaugeValueOptions.metricName)) { - throw new Error(`Value: metric is not selected. [See options: Value -> Metric]`); + constructor(grafanaSeriesList: any[], private gaugeValueOptions: ValueOptions) { + if (_.isEmpty(grafanaSeriesList)) { + throw new Error(`No metrics has been provided`); + } + if (_.isEmpty(this.gaugeValueOptions?.metricName)) { + const serie = _.first(grafanaSeriesList); + this._seriesList = this._updateSeriesListWithChartwerkParams([serie]); + return; } this._selectedSerieName = this.gaugeValueOptions.metricName; diff --git a/src/module.ts b/src/module.ts index a67da81..de92132 100644 --- a/src/module.ts +++ b/src/module.ts @@ -49,12 +49,12 @@ export const plugin = new PanelPlugin(Panel).setPanelOptions((buil category: ['Value'], showIf: (config) => config.visualizationType === Pod.GAUGE, }) - // TODO: defaults? .addCustomEditor({ id: 'min', name: 'Min', path: 'gauge.min', category: ['Extremum'], + defaultValue: { useMetric: false, value: 0 }, showIf: (config) => config.visualizationType === Pod.GAUGE, editor: UseMetricEditor as any, }) @@ -63,6 +63,7 @@ export const plugin = new PanelPlugin(Panel).setPanelOptions((buil name: 'Max', path: 'gauge.max', category: ['Extremum'], + defaultValue: { useMetric: false }, showIf: (config) => config.visualizationType === Pod.GAUGE, editor: UseMetricEditor as any, }) diff --git a/src/plugin.json b/src/plugin.json index a5440c2..56e2a86 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -4,7 +4,7 @@ "name": "Chartwerk", "id": "corpglory-chartwerk-panel", "info": { - "description": "", + "description": "Chartwerk panel with extended chart customization", "author": { "name": "CorpGlory Inc.", "url": "https://corpglory.com" @@ -17,11 +17,15 @@ "links": [ { "name": "Website", - "url": "https://gitlab.com/chartwerk/grafana-chartwerk-panel" + "url": "https://chartwerk.io/" }, { "name": "License", "url": "https://gitlab.com/chartwerk/grafana-chartwerk-panel/blob/main/LICENSE" + }, + { + "name": "Gitlab", + "url": "https://gitlab.com/chartwerk/grafana-chartwerk-panel/" } ], "screenshots": [ @@ -34,7 +38,7 @@ "updated": "%TODAY%" }, "dependencies": { - "grafanaDependency": ">=7.0.0", + "grafanaDependency": ">=8.3.0", "plugins": [] } } diff --git a/yarn.lock b/yarn.lock index d3c38f8..45d6183 100644 --- a/yarn.lock +++ b/yarn.lock @@ -902,27 +902,27 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz#fe364f025ba74f6de6c837a84ef44bdb1d61e68f" integrity sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w== -"@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== +"@chartwerk/bar-pod@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@chartwerk/bar-pod/-/bar-pod-0.6.2.tgz#780089eed241795ff964639f38305cd2fa7a89d6" + integrity sha512-2cmuHjdNRo3230oLI1ZRjLANLKfuI5f299WcBAL0eP/6DF5NB8TNV+WrA/Lw+bt9ySop/CtIFEswKhD3Sy+0dA== dependencies: - "@chartwerk/core" "^0.5.0" + "@chartwerk/core" latest -"@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== +"@chartwerk/core@latest": + version "0.6.9" + resolved "https://registry.yarnpkg.com/@chartwerk/core/-/core-0.6.9.tgz#9d63844b5935de8362f6f3440159d85040116c60" + integrity sha512-9vv1LDAoR64iS2Nxdc2YqCmWNEf3tC2bULk20K8KFA6oVQmA1imdgFJSUv4cvm7Y9VVtPxlL1wDzIjGiLzeVcw== dependencies: 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== +"@chartwerk/gauge-pod@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@chartwerk/gauge-pod/-/gauge-pod-0.6.2.tgz#7725394cd65acaaaa81cabb93a0b03e146c10362" + integrity sha512-L26hsvHCJruxfIJjXBsgcw2vszKMcMYcsXLGLy9Gy02hETpHR1pTKosgXPEpQqvQBRz+5WX+aL3x6yao/Elg/Q== dependencies: - "@chartwerk/core" "^0.5.0" + "@chartwerk/core" latest "@cnakazawa/watch@^1.0.3": version "1.0.4" From 0725f36d6251afe9c502d299a86366effa5f6f10 Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 15 Jul 2022 20:36:27 +0300 Subject: [PATCH 6/6] make zoom-in work --- package.json | 2 +- src/components/Panel.tsx | 5 +- src/models/options/barOptions.ts | 128 ++----------------------------- yarn.lock | 8 +- 4 files changed, 15 insertions(+), 128 deletions(-) diff --git a/package.json b/package.json index 5ad0aee..2810cf1 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "license": "GPL V3", "devDependencies": { "@chartwerk/gauge-pod": "^0.6.2", - "@chartwerk/bar-pod": "^0.6.2", + "@chartwerk/bar-pod": "^0.6.3", "@grafana/data": "latest", "@grafana/toolkit": "latest", "@grafana/ui": "latest", diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 481bf56..6bf5f7b 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -18,9 +18,10 @@ import * as _ from 'lodash'; interface Props extends PanelProps {} -export function Panel({ options, data, width, height, timeZone, timeRange, onChangeTimeRange }: Props) { +export function Panel({ options, data, width, height, timeRange, onChangeTimeRange }: Props) { const grafanaSeriesList = getGrafanaSeriesList(data, timeRange); 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); @@ -33,7 +34,7 @@ export function Panel({ options, data, width, height, timeZone, timeRange, onCha break; case Pod.BAR: const barSeries = new BarSeries(grafanaSeriesList).getChartwerkSeries(); - const chartwerkBarOptions = new BarOptions(grafanaSeriesList, options).getChartwerkOptions(); + const chartwerkBarOptions = new BarOptions(grafanaSeriesList, onChangeTimeRange).getChartwerkOptions(); console.log('data', barSeries, chartwerkBarOptions); pod = new ChartwerkBarPod((chartContainer as any).current, barSeries, chartwerkBarOptions); break; diff --git a/src/models/options/barOptions.ts b/src/models/options/barOptions.ts index 4966453..81dc5da 100644 --- a/src/models/options/barOptions.ts +++ b/src/models/options/barOptions.ts @@ -1,124 +1,17 @@ -import { PanelOptions, Aggregation, Threshold, Icon, IconPosition, Condition } from 'types'; +import { AbsoluteTimeRange } from '@grafana/data'; -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}`); - } + constructor(private grafanaSeriesList: any[], private changeTimeRange: (timeRange: AbsoluteTimeRange) => void) { + console.log(this.grafanaSeriesList) } getChartwerkOptions(): any { return { axis: { x: { - format: 'custom', - valueFormatter: (value: any) => { - return 'L' + value; - }, + format: 'time', }, y: { format: 'custom', @@ -132,6 +25,7 @@ export class BarOptions { matching: false, zoomEvents: { scroll: { zoom: { isActive: false }, pan: { isActive: false } }, + mouse: { doubleClick: { isActive: false } }, }, annotations: [ { key: 'm-1', color: 'red' }, @@ -139,17 +33,9 @@ export class BarOptions { ], eventsCallbacks: { zoomIn: (range: any) => { - console.log('range', range); - }, + this.changeTimeRange({ from: range[0][0], to: range[0][1] }); + } }, }; } - - 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/yarn.lock b/yarn.lock index 45d6183..f824bd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -902,10 +902,10 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz#fe364f025ba74f6de6c837a84ef44bdb1d61e68f" integrity sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w== -"@chartwerk/bar-pod@^0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@chartwerk/bar-pod/-/bar-pod-0.6.2.tgz#780089eed241795ff964639f38305cd2fa7a89d6" - integrity sha512-2cmuHjdNRo3230oLI1ZRjLANLKfuI5f299WcBAL0eP/6DF5NB8TNV+WrA/Lw+bt9ySop/CtIFEswKhD3Sy+0dA== +"@chartwerk/bar-pod@^0.6.3": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@chartwerk/bar-pod/-/bar-pod-0.6.3.tgz#e5ef980cd51d59050949124cefc55d20039b5567" + integrity sha512-mspgcW3YhCiWQw371+7IOFAonIlrNtX9IhAo1zPOTdXWFHkb5JvcR/hnqhOBAenM2zc7xjZKeih/wkbmIqXjHw== dependencies: "@chartwerk/core" latest