From dfeb69dc1781be0d1e2dcb6a1312e621d682b091 Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Thu, 21 Oct 2021 15:56:41 -0700 Subject: [PATCH] Geomap: Add style rules UI for geoJSON map layer (#40735) * Geomap: Show multiple layers in ui Co-authored-by: Ryan McKinley * Geomap: Add geojson style rules ui * add style rule editor component * update and change to two row styles Co-authored-by: Ryan McKinley --- .../geomap/editor/GeomapStyleRulesEditor.tsx | 65 +++++++ .../panel/geomap/editor/StyleRuleEditor.tsx | 163 ++++++++++++++++++ .../panel/geomap/layers/data/geojsonMapper.ts | 54 ++++-- public/app/plugins/panel/geomap/module.tsx | 27 ++- 4 files changed, 283 insertions(+), 26 deletions(-) create mode 100644 public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx create mode 100644 public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx diff --git a/public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx b/public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx new file mode 100644 index 00000000000..4b2c8ffe6af --- /dev/null +++ b/public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx @@ -0,0 +1,65 @@ +import React, { FC, useCallback } from 'react'; +import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data'; +import { ComparisonOperation, FeatureStyleConfig } from '../types'; +import { Button } from '@grafana/ui'; +import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonMapper'; +import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor'; + +export const GeomapStyleRulesEditor: FC> = (props) => { + const { value, onChange, context } = props; + + const OPTIONS = getComparisonOperatorOptions(); + + const onAddRule = useCallback(() => { + onChange([...value, DEFAULT_STYLE_RULE]); + }, [onChange, value]); + + const onRuleChange = useCallback( + (idx) => (style: FeatureStyleConfig | undefined) => { + const copyStyles = [...value]; + if (style) { + copyStyles[idx] = style; + } else { + //assume undefined is only returned on delete + copyStyles.splice(idx, 1); + } + onChange(copyStyles); + }, + [onChange, value] + ); + + const styleOptions = + value && + value.map((style, idx: number) => { + const itemSettings: StandardEditorsRegistryItem = { + settings: { options: OPTIONS }, + } as any; + + return ( + + ); + }); + + return ( + <> + {styleOptions} + + + ); +}; + +const getComparisonOperatorOptions = () => { + const options = []; + for (const value of Object.values(ComparisonOperation)) { + options.push({ value: value, label: value }); + } + return options; +}; diff --git a/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx b/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx new file mode 100644 index 00000000000..87b1f315dd7 --- /dev/null +++ b/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx @@ -0,0 +1,163 @@ +import React, { ChangeEvent, FC, useCallback } from 'react'; +import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data'; +import { ComparisonOperation, FeatureStyleConfig } from '../types'; +import { Button, ColorPicker, InlineField, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui'; +import { css } from '@emotion/css'; +import { NumberInput } from 'app/features/dimensions/editors/NumberInput'; + +export interface StyleRuleEditorSettings { + options: SelectableValue[]; +} + +export const StyleRuleEditor: FC> = ( + props +) => { + const { value, onChange, item } = props; + const settings: StyleRuleEditorSettings = item.settings; + + const styles = useStyles2(getStyles); + + const LABEL_WIDTH = 10; + + const onChangeComparisonProperty = useCallback( + (e: ChangeEvent) => { + onChange({ + ...value, + rule: { + ...value.rule, + property: e.currentTarget.value, + operation: value.rule?.operation ?? ComparisonOperation.EQ, + value: value.rule?.value ?? '', + }, + }); + }, + [onChange, value] + ); + + const onChangeComparison = useCallback( + (selection: SelectableValue) => { + onChange({ + ...value, + rule: { + ...value.rule, + operation: selection.value ?? ComparisonOperation.EQ, + property: value.rule?.property ?? '', + value: value.rule?.value ?? '', + }, + }); + }, + [onChange, value] + ); + + const onChangeComparisonValue = useCallback( + (e: ChangeEvent) => { + onChange({ + ...value, + rule: { + ...value.rule, + value: e.currentTarget.value, + operation: value.rule?.operation ?? ComparisonOperation.EQ, + property: value.rule?.property ?? '', + }, + }); + }, + [onChange, value] + ); + + const onChangeColor = useCallback( + (c: string) => { + onChange({ ...value, fillColor: c }); + }, + [onChange, value] + ); + + const onChangeStrokeWidth = useCallback( + (num: number | undefined) => { + onChange({ ...value, strokeWidth: num ?? value.strokeWidth ?? 1 }); + }, + [onChange, value] + ); + + const onDelete = useCallback(() => { + onChange(undefined); + }, [onChange]); + + return ( +
+ + + + + + + + + + + + + + + + + +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + rule: css` + margin-bottom: ${theme.spacing(1)}; + `, + row: css` + display: flex; + margin-bottom: 4px; + `, + inline: css` + margin-bottom: 0; + margin-left: 4px; + `, + color: css` + align-items: center; + margin-bottom: 0; + margin-right: 4px; + `, + button: css` + margin-left: 4px; + `, +}); diff --git a/public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts b/public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts index 309086d590c..f5507acf912 100644 --- a/public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts +++ b/public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts @@ -7,15 +7,16 @@ import { Feature } from 'ol'; import { Geometry } from 'ol/geom'; import { getGeoMapStyle } from '../../utils/getGeoMapStyle'; import { checkFeatureMatchesStyleRule } from '../../utils/checkFeatureMatchesStyleRule'; -import { FeatureStyleConfig } from '../../types'; +import { ComparisonOperation, FeatureStyleConfig } from '../../types'; import { Stroke, Style } from 'ol/style'; import { FeatureLike } from 'ol/Feature'; +import { GeomapStyleRulesEditor } from '../../editor/GeomapStyleRulesEditor'; export interface GeoJSONMapperConfig { // URL for a geojson file src?: string; // Styles that can be applied - styles?: FeatureStyleConfig[]; + styles: FeatureStyleConfig[]; } const defaultOptions: GeoJSONMapperConfig = { @@ -23,6 +24,16 @@ const defaultOptions: GeoJSONMapperConfig = { styles: [], }; +export const DEFAULT_STYLE_RULE: FeatureStyleConfig = { + fillColor: '#1F60C4', + strokeWidth: 1, + rule: { + property: '', + operation: ComparisonOperation.EQ, + value: '', + }, +}; + export const geojsonMapper: MapLayerRegistryItem = { id: 'geojson-value-mapper', name: 'Map values to GeoJSON file', @@ -44,8 +55,8 @@ export const geojsonMapper: MapLayerRegistryItem = { const defaultStyle = new Style({ stroke: new Stroke({ - color: '#1F60C4', - width: 1, + color: DEFAULT_STYLE_RULE.fillColor, + width: DEFAULT_STYLE_RULE.strokeWidth, }), }); @@ -79,20 +90,27 @@ export const geojsonMapper: MapLayerRegistryItem = { // Geojson source url registerOptionsUI: (builder) => { - builder.addSelect({ - path: 'config.src', - name: 'GeoJSON URL', - settings: { - options: [ - { label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' }, - { label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' }, - ], - allowCustomValue: true, - }, - defaultValue: defaultOptions.src, - }); + builder + .addSelect({ + path: 'config.src', + name: 'GeoJSON URL', + settings: { + options: [ + { label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' }, + { label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' }, + ], + allowCustomValue: true, + }, + defaultValue: defaultOptions.src, + }) + .addCustomEditor({ + id: 'config.styles', + path: 'config.styles', + name: 'Style Rules', + editor: GeomapStyleRulesEditor, + settings: {}, + defaultValue: [], + }); }, - - // fill in the default values defaultOptions, }; diff --git a/public/app/plugins/panel/geomap/module.tsx b/public/app/plugins/panel/geomap/module.tsx index 73d7507086d..90ee0aefd55 100644 --- a/public/app/plugins/panel/geomap/module.tsx +++ b/public/app/plugins/panel/geomap/module.tsx @@ -53,14 +53,25 @@ export const plugin = new PanelPlugin(GeomapPanel) ); } - builder.addNestedOptions( - getLayerEditor({ - category: ['Data layer'], - path: 'layers[0]', // only one for now - basemaps: false, - current: context.options?.layers?.[0], - }) - ); + let layerCount = context.options?.layers?.length; + if (layerCount == null || layerCount < 1) { + layerCount = 1; + } + + for (let i = 0; i < layerCount; i++) { + let name = 'Data layer'; + if (i > 0) { + name += ` (${i + 1})`; + } + builder.addNestedOptions( + getLayerEditor({ + category: [name], + path: `layers[${i}]`, // only one for now + basemaps: false, + current: context.options?.layers?.[i], + }) + ); + } // The controls section category = ['Map controls'];