From 4d9d5b4839158679f6606095a589c27e4ca81eb6 Mon Sep 17 00:00:00 2001 From: rozetko Date: Wed, 4 May 2022 15:28:15 +0400 Subject: [PATCH 1/2] options: remove Value input --- src/models/series.ts | 19 ++++++------------- src/module.ts | 22 +++++++--------------- src/types.ts | 10 +++++++--- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/models/series.ts b/src/models/series.ts index 1c34f98..7949228 100644 --- a/src/models/series.ts +++ b/src/models/series.ts @@ -1,22 +1,19 @@ import { PanelData, TimeRange } from '@grafana/data'; import { DataProcessor } from '../grafana/data_processor'; -import { ExtremumOptions } from 'types'; +import { ValueOptions } from 'types'; import * as _ from 'lodash'; -// Convert Grafana options into Chartwerk options + +// Convert Grafana series into Chartwerk series export class Series { - private processor; + private processor; private _seriesList; private _selectedSerieName; - constructor(grafanaData: PanelData, timeRange: TimeRange, private gaugeValueOptions: ExtremumOptions) { - if(this._isSerieOneValue()) { - this._seriesList = [{ datapoints: [[0, gaugeValueOptions.value]] }]; - return; - } - if(this.gaugeValueOptions.useMetric && _.isEmpty(this.gaugeValueOptions.metricName)) { + constructor(grafanaData: PanelData, timeRange: TimeRange, private gaugeValueOptions: ValueOptions) { + if(_.isEmpty(this.gaugeValueOptions.metricName)) { throw new Error(`Value or metric is not selected.`); } @@ -52,8 +49,4 @@ export class Series { }; }); } - - private _isSerieOneValue(): boolean { - return !this.gaugeValueOptions.useMetric; - } } diff --git a/src/module.ts b/src/module.ts index 84217f1..c5c3558 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,7 +1,9 @@ -import { PanelPlugin } from '@grafana/data'; import { PanelOptions, Pod } from './types'; import { Panel } from './components/Panel'; +import { PanelPlugin } from '@grafana/data'; + + export const plugin = new PanelPlugin(Panel).setPanelOptions((builder) => { return builder .addRadio({ @@ -29,25 +31,14 @@ export const plugin = new PanelPlugin(Panel).setPanelOptions((buil ], }, }) - .addNumberInput({ - name: 'Value', - path: 'gauge.value.value', - category: ['Extremum'], - showIf: (config) => config.visualizationType === Pod.GAUGE && !config.gauge.value.useMetric, - }) + .addFieldNamePicker({ name: 'Value', path: 'gauge.value.metricName', category: ['Extremum'], - showIf: (config) => config.visualizationType === Pod.GAUGE && config.gauge.value.useMetric, - }) - .addBooleanSwitch({ - path: 'gauge.value.useMetric', - name: 'Use metric', - defaultValue: false, - category: ['Extremum'], - showIf: (config) => config.visualizationType === Pod.GAUGE, + showIf: (config) => config.visualizationType === Pod.GAUGE }) + .addNumberInput({ path: 'gauge.min.value', name: 'Min', @@ -67,6 +58,7 @@ export const plugin = new PanelPlugin(Panel).setPanelOptions((buil category: ['Extremum'], showIf: (config) => config.visualizationType === Pod.GAUGE, }) + .addNumberInput({ path: 'gauge.max.value', name: 'Max', diff --git a/src/types.ts b/src/types.ts index 22e89bb..614140d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,10 +8,14 @@ export interface PanelOptions { } export type ExtremumOptions = { - value?: number; useMetric: false; - metricName: string; -}; + value?: number; + metricName?: string; +} + +export type ValueOptions = { + metricName?: string +} export enum Pod { LINE = 'Line', From aa11cada971728f72c1ffc65e1e9f537721a06a0 Mon Sep 17 00:00:00 2001 From: rozetko Date: Wed, 4 May 2022 19:04:36 +0400 Subject: [PATCH 2/2] custom icons editor --- src/components/editors/IconsEditor.tsx | 222 +++++++++++++++++++++ src/grafana/MatchersUI/FieldNamePicker.tsx | 46 +++++ src/grafana/MatchersUI/types.ts | 204 +++++++++++++++++++ src/grafana/MatchersUI/utils.ts | 116 +++++++++++ src/module.ts | 13 +- src/types.ts | 6 + 6 files changed, 606 insertions(+), 1 deletion(-) create mode 100644 src/components/editors/IconsEditor.tsx create mode 100644 src/grafana/MatchersUI/FieldNamePicker.tsx create mode 100644 src/grafana/MatchersUI/types.ts create mode 100644 src/grafana/MatchersUI/utils.ts diff --git a/src/components/editors/IconsEditor.tsx b/src/components/editors/IconsEditor.tsx new file mode 100644 index 0000000..391a0e4 --- /dev/null +++ b/src/components/editors/IconsEditor.tsx @@ -0,0 +1,222 @@ +import { IconPosition } from 'types'; + +import { GrafanaTheme, SelectableValue, StandardEditorProps } from '@grafana/data'; +import { Button, HorizontalGroup, IconButton, Input, RadioButtonGroup, Slider, stylesFactory, ThemeContext } from '@grafana/ui'; +import { FieldNamePicker } from '../../grafana/MatchersUI/FieldNamePicker'; + +import { css } from 'emotion'; +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', + value: IconPosition.UPPER_LEFT, + }, + { + label: 'Middle', + value: IconPosition.MIDDLE, + }, + { + label: 'Upper-Right', + value: IconPosition.UPPER_RIGHT, + }, +]; + +const conditionOptions: Array> = [ + { + label: '>=', + value: Condition.GREATER_OR_EQUAL, + }, + { + label: '>', + value: Condition.GREATER, + }, + { + label: '=', + value: Condition.EQUAL, + }, + { + label: '<', + value: Condition.LESS, + }, + { + label: '<=', + value: Condition.LESS_OR_EQUAL, + }, +]; + +const DEFAULT_ICON: IconConfig = { + position: IconPosition.UPPER_LEFT, + url: '', + size: 40, + metrics: [], + conditions: [], + values: [], +} + +const fieldNamePickerSettings = { + settings: { width: 24 }, +} as any; + +export function IconsEditor({ onChange, value, context }: StandardEditorProps>) { + const icons = value; + + const addIcon = () => { + onChange( + _.concat(icons, _.cloneDeep(DEFAULT_ICON)) + ); + }; + + const removeIcon = (idx: number) => { + onChange( + _.filter(icons, (icon, iconIdx) => iconIdx !== idx) + ); + }; + + const addCondition = (iconIdx:number) => { + icons[iconIdx].conditions.push(Condition.GREATER_OR_EQUAL); + icons[iconIdx].metrics.push(''); + icons[iconIdx].values.push(0); + + onChange(icons); + }; + + const removeCondition = (iconIdx: number, conditionIdx: number) => { + icons[iconIdx].conditions.splice(conditionIdx, 1); + icons[iconIdx].metrics.splice(conditionIdx, 1); + icons[iconIdx].values.splice(conditionIdx, 1); + + onChange(icons); + }; + + const onIconFieldChange = (iconIdx: number, field: keyof IconConfig, value: any) => { + // @ts-ignore + icons[iconIdx][field] = value; + + onChange(icons); + }; + + const onConditionChange = (iconIdx: number, conditionIdx: number, field: keyof IconConfig, value: any) => { + console.log(value) + // @ts-ignore + icons[iconIdx][field][conditionIdx] = value; + + onChange(icons); + }; + + return ( + + {(theme) => { + const styles = getStyles(theme.v1); + return ( +
+
+ { + icons.map((icon, iconIdx) => { + return ( +
+ removeIcon(iconIdx)}> + onIconFieldChange(iconIdx, 'url', (evt.target as any).value)} + /> + onIconFieldChange(iconIdx, 'position', newVal)} + /> + onIconFieldChange(iconIdx, 'size', newVal)} + /> + + { + icon.conditions.map((condition, conditionIdx) => { + return ( + + onConditionChange(iconIdx, conditionIdx, 'metrics', newVal) } + item={fieldNamePickerSettings} + /> + onConditionChange(iconIdx, conditionIdx, 'conditions', newVal)} + /> + onConditionChange(iconIdx, conditionIdx, 'values', (evt.target as any).value)} + /> + removeCondition(iconIdx, conditionIdx)}> + + ) + }) + } + +
+ ) + }) + } +
+ +
+ ) + } + } +
+ ) +} + +interface IconsEditorStyles { + icons: string; + icon: string; +} + +const getStyles = stylesFactory((theme: GrafanaTheme): IconsEditorStyles => { + return { + icons: css` + display: flex; + flex-direction: column; + margin-bottom: ${theme.spacing.formSpacingBase * 2}px; + `, + icon: css` + margin-bottom: ${theme.spacing.sm}; + border-bottom: 1px solid ${theme.colors.panelBorder}; + padding-bottom: ${theme.spacing.formSpacingBase * 2}px; + + &:last-child { + border: none; + margin-bottom: 0; + } + `, + } +}); diff --git a/src/grafana/MatchersUI/FieldNamePicker.tsx b/src/grafana/MatchersUI/FieldNamePicker.tsx new file mode 100644 index 0000000..044526f --- /dev/null +++ b/src/grafana/MatchersUI/FieldNamePicker.tsx @@ -0,0 +1,46 @@ +// copied from https://github.com/grafana/grafana/blob/3c6e0e8ef85048af952367751e478c08342e17b4/packages/grafana-ui/src/components/MatchersUI/FieldNamePicker.tsx +import React, { useCallback } from 'react'; + +import { FieldNamePickerConfigSettings, SelectableValue, StandardEditorProps } from '@grafana/data'; + +import { Select } from '@grafana/ui'; + +import { useFieldDisplayNames, useSelectOptions, frameHasName } from './utils'; + +// Pick a field name out of the fulds +export const FieldNamePicker: React.FC> = ({ + value, + onChange, + context, + item, +}) => { + const settings: FieldNamePickerConfigSettings = item.settings ?? {}; + const names = useFieldDisplayNames(context.data, settings?.filter); + const selectOptions = useSelectOptions(names, value); + + const onSelectChange = useCallback( + (selection?: SelectableValue) => { + if (selection && !frameHasName(selection.value, names)) { + return; // can not select name that does not exist? + } + return onChange(selection?.value); + }, + [names, onChange] + ); + + const selectedOption = selectOptions.find((v: any) => v.value === value); + return ( + <> +