mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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 { Geometry } from 'ol/geom';
|
||||||
import { getGeoMapStyle } from '../../utils/getGeoMapStyle';
|
import { getGeoMapStyle } from '../../utils/getGeoMapStyle';
|
||||||
import { checkFeatureMatchesStyleRule } from '../../utils/checkFeatureMatchesStyleRule';
|
import { checkFeatureMatchesStyleRule } from '../../utils/checkFeatureMatchesStyleRule';
|
||||||
import { FeatureStyleConfig } from '../../types';
|
import { ComparisonOperation, FeatureStyleConfig } from '../../types';
|
||||||
import { Stroke, Style } from 'ol/style';
|
import { Stroke, Style } from 'ol/style';
|
||||||
import { FeatureLike } from 'ol/Feature';
|
import { FeatureLike } from 'ol/Feature';
|
||||||
|
import { GeomapStyleRulesEditor } from '../../editor/GeomapStyleRulesEditor';
|
||||||
export interface GeoJSONMapperConfig {
|
export interface GeoJSONMapperConfig {
|
||||||
// URL for a geojson file
|
// URL for a geojson file
|
||||||
src?: string;
|
src?: string;
|
||||||
|
|
||||||
// Styles that can be applied
|
// Styles that can be applied
|
||||||
styles?: FeatureStyleConfig[];
|
styles: FeatureStyleConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: GeoJSONMapperConfig = {
|
const defaultOptions: GeoJSONMapperConfig = {
|
||||||
@ -23,6 +24,16 @@ const defaultOptions: GeoJSONMapperConfig = {
|
|||||||
styles: [],
|
styles: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_STYLE_RULE: FeatureStyleConfig = {
|
||||||
|
fillColor: '#1F60C4',
|
||||||
|
strokeWidth: 1,
|
||||||
|
rule: {
|
||||||
|
property: '',
|
||||||
|
operation: ComparisonOperation.EQ,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||||
id: 'geojson-value-mapper',
|
id: 'geojson-value-mapper',
|
||||||
name: 'Map values to GeoJSON file',
|
name: 'Map values to GeoJSON file',
|
||||||
@ -44,8 +55,8 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
|||||||
|
|
||||||
const defaultStyle = new Style({
|
const defaultStyle = new Style({
|
||||||
stroke: new Stroke({
|
stroke: new Stroke({
|
||||||
color: '#1F60C4',
|
color: DEFAULT_STYLE_RULE.fillColor,
|
||||||
width: 1,
|
width: DEFAULT_STYLE_RULE.strokeWidth,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,20 +90,27 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
|||||||
|
|
||||||
// Geojson source url
|
// Geojson source url
|
||||||
registerOptionsUI: (builder) => {
|
registerOptionsUI: (builder) => {
|
||||||
builder.addSelect({
|
builder
|
||||||
path: 'config.src',
|
.addSelect({
|
||||||
name: 'GeoJSON URL',
|
path: 'config.src',
|
||||||
settings: {
|
name: 'GeoJSON URL',
|
||||||
options: [
|
settings: {
|
||||||
{ label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' },
|
options: [
|
||||||
{ label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' },
|
{ label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' },
|
||||||
],
|
{ label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' },
|
||||||
allowCustomValue: true,
|
],
|
||||||
},
|
allowCustomValue: true,
|
||||||
defaultValue: defaultOptions.src,
|
},
|
||||||
});
|
defaultValue: defaultOptions.src,
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.styles',
|
||||||
|
path: 'config.styles',
|
||||||
|
name: 'Style Rules',
|
||||||
|
editor: GeomapStyleRulesEditor,
|
||||||
|
settings: {},
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// fill in the default values
|
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
};
|
};
|
||||||
|
@ -53,14 +53,25 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.addNestedOptions(
|
let layerCount = context.options?.layers?.length;
|
||||||
getLayerEditor({
|
if (layerCount == null || layerCount < 1) {
|
||||||
category: ['Data layer'],
|
layerCount = 1;
|
||||||
path: 'layers[0]', // only one for now
|
}
|
||||||
basemaps: false,
|
|
||||||
current: context.options?.layers?.[0],
|
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
|
// The controls section
|
||||||
category = ['Map controls'];
|
category = ['Map controls'];
|
||||||
|
Loading…
Reference in New Issue
Block a user