mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 16:15:42 -06:00
Geomap: Add style rules UI for geoJSON map layer (#40735)
* Geomap: Show multiple layers in ui Co-authored-by: Ryan McKinley <ryantxu@users.noreply.github.com> * Geomap: Add geojson style rules ui * add style rule editor component * update and change to two row styles Co-authored-by: Ryan McKinley <ryantxu@users.noreply.github.com>
This commit is contained in:
parent
97df4a57f4
commit
dfeb69dc17
@ -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<StandardEditorProps<FeatureStyleConfig[], any, any>> = (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<any, StyleRuleEditorSettings> = {
|
||||
settings: { options: OPTIONS },
|
||||
} as any;
|
||||
|
||||
return (
|
||||
<StyleRuleEditor
|
||||
value={style}
|
||||
onChange={onRuleChange(idx)}
|
||||
context={context}
|
||||
item={itemSettings}
|
||||
key={`${idx}-${style.rule}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{styleOptions}
|
||||
<Button size="sm" icon="plus" onClick={onAddRule} variant="secondary" aria-label={'Add geomap style rule'}>
|
||||
{'Add style rule'}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getComparisonOperatorOptions = () => {
|
||||
const options = [];
|
||||
for (const value of Object.values(ComparisonOperation)) {
|
||||
options.push({ value: value, label: value });
|
||||
}
|
||||
return options;
|
||||
};
|
163
public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx
Normal file
163
public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx
Normal file
@ -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<StandardEditorProps<FeatureStyleConfig, any, any, StyleRuleEditorSettings>> = (
|
||||
props
|
||||
) => {
|
||||
const { value, onChange, item } = props;
|
||||
const settings: StyleRuleEditorSettings = item.settings;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const LABEL_WIDTH = 10;
|
||||
|
||||
const onChangeComparisonProperty = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className={styles.rule}>
|
||||
<InlineFieldRow className={styles.row}>
|
||||
<InlineField label="Rule" labelWidth={LABEL_WIDTH} grow={true}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={'Feature property'}
|
||||
value={`${value?.rule?.property}`}
|
||||
onChange={onChangeComparisonProperty}
|
||||
aria-label={'Feature property'}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField className={styles.inline} grow={true}>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
value={`${value?.rule?.operation}` ?? ComparisonOperation.EQ}
|
||||
options={settings.options}
|
||||
onChange={onChangeComparison}
|
||||
aria-label={'Comparison operator'}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField className={styles.inline} grow={true}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={'value'}
|
||||
value={`${value?.rule?.value}`}
|
||||
onChange={onChangeComparisonValue}
|
||||
aria-label={'Comparison value'}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow className={styles.row}>
|
||||
<InlineField label="Style" labelWidth={LABEL_WIDTH} className={styles.color}>
|
||||
<ColorPicker color={value?.fillColor} onChange={onChangeColor} />
|
||||
</InlineField>
|
||||
<InlineField label="Stroke" className={styles.inline} grow={true}>
|
||||
<NumberInput
|
||||
value={value?.strokeWidth ?? 1}
|
||||
min={1}
|
||||
max={20}
|
||||
step={0.5}
|
||||
aria-label={'Stroke width'}
|
||||
onChange={onChangeStrokeWidth}
|
||||
/>
|
||||
</InlineField>
|
||||
<Button
|
||||
size="md"
|
||||
icon="trash-alt"
|
||||
onClick={() => onDelete()}
|
||||
variant="secondary"
|
||||
aria-label={'Delete style rule'}
|
||||
className={styles.button}
|
||||
></Button>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
`,
|
||||
});
|
@ -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<GeoJSONMapperConfig> = {
|
||||
id: 'geojson-value-mapper',
|
||||
name: 'Map values to GeoJSON file',
|
||||
@ -44,8 +55,8 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
|
||||
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<GeoJSONMapperConfig> = {
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
@ -53,14 +53,25 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(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'];
|
||||
|
Loading…
Reference in New Issue
Block a user