mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
parent
4710572d2a
commit
03f54577a5
@ -1,17 +1,27 @@
|
|||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import { Button, useTheme2 } from '@grafana/ui';
|
||||||
import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
|
import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
|
||||||
|
|
||||||
import { FeatureStyleConfig } from '../types';
|
import { FeatureStyleConfig } from '../types';
|
||||||
import { Button } from '@grafana/ui';
|
|
||||||
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
|
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
|
||||||
import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
|
import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
|
||||||
|
import { defaultStyleConfig } from '../style/types';
|
||||||
|
|
||||||
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => {
|
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => {
|
||||||
const { value, onChange, context, item } = props;
|
const { value, onChange, context, item } = props;
|
||||||
|
const theme = useTheme2();
|
||||||
|
|
||||||
const settings = item.settings;
|
const settings = item.settings;
|
||||||
const onAddRule = useCallback(() => {
|
const onAddRule = useCallback(() => {
|
||||||
onChange([...value, DEFAULT_STYLE_RULE]);
|
const { palette } = theme.visualization;
|
||||||
}, [onChange, value]);
|
const color = {
|
||||||
|
fixed: palette[Math.floor(Math.random() * palette.length)],
|
||||||
|
};
|
||||||
|
|
||||||
|
const newRule = [...value, { ...DEFAULT_STYLE_RULE, style: { ...defaultStyleConfig, color } }];
|
||||||
|
|
||||||
|
onChange(newRule);
|
||||||
|
}, [onChange, value, theme.visualization]);
|
||||||
|
|
||||||
const onRuleChange = useCallback(
|
const onRuleChange = useCallback(
|
||||||
(idx) => (style: FeatureStyleConfig | undefined) => {
|
(idx) => (style: FeatureStyleConfig | undefined) => {
|
||||||
|
@ -12,7 +12,6 @@ import { getUniqueFeatureValues, LayerContentInfo } from '../utils/getFeatures';
|
|||||||
import { FeatureLike } from 'ol/Feature';
|
import { FeatureLike } from 'ol/Feature';
|
||||||
import { getSelectionInfo } from '../utils/selection';
|
import { getSelectionInfo } from '../utils/selection';
|
||||||
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
|
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
|
||||||
import { isNumber } from 'lodash';
|
|
||||||
|
|
||||||
export interface StyleRuleEditorSettings {
|
export interface StyleRuleEditorSettings {
|
||||||
features: Observable<FeatureLike[]>;
|
features: Observable<FeatureLike[]>;
|
||||||
@ -21,6 +20,7 @@ export interface StyleRuleEditorSettings {
|
|||||||
|
|
||||||
const comparators = [
|
const comparators = [
|
||||||
{ label: '==', value: ComparisonOperation.EQ },
|
{ label: '==', value: ComparisonOperation.EQ },
|
||||||
|
{ label: '!=', value: ComparisonOperation.NEQ },
|
||||||
{ label: '>', value: ComparisonOperation.GT },
|
{ label: '>', value: ComparisonOperation.GT },
|
||||||
{ label: '>=', value: ComparisonOperation.GTE },
|
{ label: '>=', value: ComparisonOperation.GTE },
|
||||||
{ label: '<', value: ComparisonOperation.LT },
|
{ label: '<', value: ComparisonOperation.LT },
|
||||||
@ -40,7 +40,21 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
|||||||
const uniqueSelectables = useMemo(() => {
|
const uniqueSelectables = useMemo(() => {
|
||||||
const key = value?.check?.property;
|
const key = value?.check?.property;
|
||||||
if (key && feats && value.check?.operation === ComparisonOperation.EQ) {
|
if (key && feats && value.check?.operation === ComparisonOperation.EQ) {
|
||||||
return getUniqueFeatureValues(feats, key).map((v) => ({ value: v, label: v }));
|
return getUniqueFeatureValues(feats, key).map((v) => {
|
||||||
|
let newValue;
|
||||||
|
let isNewValueNumber = !isNaN(Number(v));
|
||||||
|
|
||||||
|
if (isNewValueNumber) {
|
||||||
|
newValue = {
|
||||||
|
value: Number(v),
|
||||||
|
label: v,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newValue = { value: v, label: v };
|
||||||
|
}
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [feats, value]);
|
}, [feats, value]);
|
||||||
@ -143,7 +157,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
|||||||
</InlineField>
|
</InlineField>
|
||||||
<InlineField className={styles.inline} grow={true}>
|
<InlineField className={styles.inline} grow={true}>
|
||||||
<>
|
<>
|
||||||
{check.operation === ComparisonOperation.EQ && (
|
{(check.operation === ComparisonOperation.EQ || check.operation === ComparisonOperation.NEQ) && (
|
||||||
<Select
|
<Select
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
placeholder={'value'}
|
placeholder={'value'}
|
||||||
@ -158,7 +172,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
|||||||
{check.operation !== ComparisonOperation.EQ && (
|
{check.operation !== ComparisonOperation.EQ && (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
key={`${check.property}/${check.operation}`}
|
key={`${check.property}/${check.operation}`}
|
||||||
value={isNumber(check.value) ? check.value : 0}
|
value={!isNaN(Number(check.value)) ? Number(check.value) : 0}
|
||||||
placeholder="numeric value"
|
placeholder="numeric value"
|
||||||
onChange={onChangeNumericValue}
|
onChange={onChangeNumericValue}
|
||||||
/>
|
/>
|
||||||
@ -183,6 +197,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
|||||||
{
|
{
|
||||||
settings: {
|
settings: {
|
||||||
simpleFixedValues: true,
|
simpleFixedValues: true,
|
||||||
|
layerInfo,
|
||||||
},
|
},
|
||||||
} as any
|
} as any
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
RadioButtonGroup,
|
RadioButtonGroup,
|
||||||
SliderValueEditor,
|
SliderValueEditor,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { useObservable } from 'react-use';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ColorDimensionEditor,
|
ColorDimensionEditor,
|
||||||
@ -28,8 +30,10 @@ import {
|
|||||||
} from 'app/features/dimensions/types';
|
} from 'app/features/dimensions/types';
|
||||||
import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
|
import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
|
||||||
import { styleUsesText } from '../../style/utils';
|
import { styleUsesText } from '../../style/utils';
|
||||||
|
import { LayerContentInfo } from '../../utils/getFeatures';
|
||||||
|
|
||||||
export interface StyleEditorOptions {
|
export interface StyleEditorOptions {
|
||||||
|
layerInfo?: Observable<LayerContentInfo>;
|
||||||
simpleFixedValues?: boolean;
|
simpleFixedValues?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,10 +83,40 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline as TextBaseline } });
|
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline as TextBaseline } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let featuresHavePoints = false;
|
||||||
|
if (item.settings?.layerInfo) {
|
||||||
|
const propertyOptions = useObservable(item.settings?.layerInfo);
|
||||||
|
featuresHavePoints = propertyOptions?.geometryType === 'point';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasTextLabel = styleUsesText(value);
|
||||||
|
|
||||||
// Simple fixed value display
|
// Simple fixed value display
|
||||||
if (item.settings?.simpleFixedValues) {
|
if (item.settings?.simpleFixedValues) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{featuresHavePoints && (
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label={'Symbol'}>
|
||||||
|
<ResourceDimensionEditor
|
||||||
|
value={value.symbol ?? defaultStyleConfig.symbol}
|
||||||
|
context={context}
|
||||||
|
onChange={onSymbolChange}
|
||||||
|
item={
|
||||||
|
{
|
||||||
|
settings: {
|
||||||
|
resourceType: 'icon',
|
||||||
|
folderName: ResourceFolderName.Marker,
|
||||||
|
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
|
||||||
|
placeholderValue: defaultStyleConfig.symbol.fixed,
|
||||||
|
showSourceRadio: false,
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
)}
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label="Color" labelWidth={10}>
|
<InlineField label="Color" labelWidth={10}>
|
||||||
<InlineLabel width={4}>
|
<InlineLabel width={4}>
|
||||||
@ -96,7 +130,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
</InlineField>
|
</InlineField>
|
||||||
</InlineFieldRow>
|
</InlineFieldRow>
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label="Opacity" labelWidth={10} grow={true}>
|
<InlineField label="Opacity" labelWidth={10} grow>
|
||||||
<SliderValueEditor
|
<SliderValueEditor
|
||||||
value={value.opacity ?? defaultStyleConfig.opacity}
|
value={value.opacity ?? defaultStyleConfig.opacity}
|
||||||
context={context}
|
context={context}
|
||||||
@ -117,8 +151,6 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasTextLabel = styleUsesText(value);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Field label={'Size'}>
|
<Field label={'Size'}>
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { MapLayerRegistryItem, MapLayerOptions, PanelData, GrafanaTheme2, PluginState } from '@grafana/data';
|
||||||
MapLayerRegistryItem,
|
|
||||||
MapLayerOptions,
|
|
||||||
PanelData,
|
|
||||||
GrafanaTheme2,
|
|
||||||
PluginState,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import Map from 'ol/Map';
|
import Map from 'ol/Map';
|
||||||
import VectorLayer from 'ol/layer/Vector';
|
import VectorLayer from 'ol/layer/Vector';
|
||||||
import VectorSource from 'ol/source/Vector';
|
import VectorSource from 'ol/source/Vector';
|
||||||
@ -129,7 +123,7 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
|||||||
// get properties for first feature to use as ui options
|
// get properties for first feature to use as ui options
|
||||||
const layerInfo = features.pipe(
|
const layerInfo = features.pipe(
|
||||||
first(),
|
first(),
|
||||||
rxjsmap((v) => getLayerPropertyInfo(v)),
|
rxjsmap((v) => getLayerPropertyInfo(v))
|
||||||
);
|
);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
@ -146,18 +140,6 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
|||||||
},
|
},
|
||||||
defaultValue: defaultOptions.src,
|
defaultValue: defaultOptions.src,
|
||||||
})
|
})
|
||||||
.addCustomEditor({
|
|
||||||
id: 'config.rules',
|
|
||||||
path: 'config.rules',
|
|
||||||
name: 'Style Rules',
|
|
||||||
description: 'Apply styles based on feature properties',
|
|
||||||
editor: GeomapStyleRulesEditor,
|
|
||||||
settings: {
|
|
||||||
features: features,
|
|
||||||
layerInfo: layerInfo,
|
|
||||||
},
|
|
||||||
defaultValue: [],
|
|
||||||
})
|
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
id: 'config.style',
|
id: 'config.style',
|
||||||
path: 'config.style',
|
path: 'config.style',
|
||||||
@ -166,8 +148,21 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
|||||||
editor: StyleEditor,
|
editor: StyleEditor,
|
||||||
settings: {
|
settings: {
|
||||||
simpleFixedValues: true,
|
simpleFixedValues: true,
|
||||||
|
layerInfo,
|
||||||
},
|
},
|
||||||
defaultValue: defaultOptions.style,
|
defaultValue: defaultOptions.style,
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.rules',
|
||||||
|
path: 'config.rules',
|
||||||
|
name: 'Style Rules',
|
||||||
|
description: 'Apply styles based on feature properties',
|
||||||
|
editor: GeomapStyleRulesEditor,
|
||||||
|
settings: {
|
||||||
|
features,
|
||||||
|
layerInfo,
|
||||||
|
},
|
||||||
|
defaultValue: [],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -57,6 +57,7 @@ export interface FeatureRuleConfig {
|
|||||||
|
|
||||||
export enum ComparisonOperation {
|
export enum ComparisonOperation {
|
||||||
EQ = 'eq',
|
EQ = 'eq',
|
||||||
|
NEQ = 'neq',
|
||||||
LT = 'lt',
|
LT = 'lt',
|
||||||
LTE = 'lte',
|
LTE = 'lte',
|
||||||
GT = 'gt',
|
GT = 'gt',
|
||||||
|
@ -58,6 +58,16 @@ describe('check if feature matches style rule', () => {
|
|||||||
feature
|
feature
|
||||||
)
|
)
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
|
expect(
|
||||||
|
checkFeatureMatchesStyleRule(
|
||||||
|
{
|
||||||
|
operation: ComparisonOperation.NEQ,
|
||||||
|
property: 'number',
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
feature
|
||||||
|
)
|
||||||
|
).toEqual(false);
|
||||||
});
|
});
|
||||||
it('can compare with strings', () => {
|
it('can compare with strings', () => {
|
||||||
const feature = new Feature({
|
const feature = new Feature({
|
||||||
@ -114,6 +124,16 @@ describe('check if feature matches style rule', () => {
|
|||||||
feature
|
feature
|
||||||
)
|
)
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
|
expect(
|
||||||
|
checkFeatureMatchesStyleRule(
|
||||||
|
{
|
||||||
|
operation: ComparisonOperation.NEQ,
|
||||||
|
property: 'string',
|
||||||
|
value: 'b',
|
||||||
|
},
|
||||||
|
feature
|
||||||
|
)
|
||||||
|
).toEqual(false);
|
||||||
});
|
});
|
||||||
it('can compare with booleans', () => {
|
it('can compare with booleans', () => {
|
||||||
const feature = new Feature({
|
const feature = new Feature({
|
||||||
@ -172,5 +192,15 @@ describe('check if feature matches style rule', () => {
|
|||||||
feature
|
feature
|
||||||
)
|
)
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
|
expect(
|
||||||
|
checkFeatureMatchesStyleRule(
|
||||||
|
{
|
||||||
|
operation: ComparisonOperation.NEQ,
|
||||||
|
property: 'boolean',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
feature
|
||||||
|
)
|
||||||
|
).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,8 @@ export const checkFeatureMatchesStyleRule = (rule: FeatureRuleConfig, feature: F
|
|||||||
switch (rule.operation) {
|
switch (rule.operation) {
|
||||||
case ComparisonOperation.EQ:
|
case ComparisonOperation.EQ:
|
||||||
return val === rule.value;
|
return val === rule.value;
|
||||||
|
case ComparisonOperation.NEQ:
|
||||||
|
return val !== rule.value;
|
||||||
case ComparisonOperation.GT:
|
case ComparisonOperation.GT:
|
||||||
return val > rule.value;
|
return val > rule.value;
|
||||||
case ComparisonOperation.GTE:
|
case ComparisonOperation.GTE:
|
||||||
|
Loading…
Reference in New Issue
Block a user