grafana/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx

229 lines
6.8 KiB
TypeScript

import { css } from '@emotion/css';
import { FeatureLike } from 'ol/Feature';
import { useCallback, useMemo } from 'react';
import { useObservable } from 'react-use';
import { Observable } from 'rxjs';
import { GrafanaTheme2, SelectableValue, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { ComparisonOperation } from '@grafana/schema';
import { Button, InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
import { comparisonOperationOptions } from '@grafana/ui/internal';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
import { defaultStyleConfig, StyleConfig } from '../style/types';
import { FeatureStyleConfig } from '../types';
import { getUniqueFeatureValues, LayerContentInfo } from '../utils/getFeatures';
import { getSelectionInfo } from '../utils/selection';
import { StyleEditor } from './StyleEditor';
export interface StyleRuleEditorSettings {
features: Observable<FeatureLike[]>;
layerInfo: Observable<LayerContentInfo>;
}
type Props = StandardEditorProps<FeatureStyleConfig, StyleRuleEditorSettings, unknown>;
export const StyleRuleEditor = ({ value, onChange, item, context }: Props) => {
const { t } = useTranslate();
const settings = item.settings;
if (!settings) {
// Shouldn't be possible to hit this block, but just in case
throw Error('Settings not found');
}
const { features, layerInfo } = settings;
const propertyOptions = useObservable(layerInfo);
const feats = useObservable(features);
const uniqueSelectables = useMemo(() => {
const key = value?.check?.property;
if (key && feats && value.check?.operation === ComparisonOperation.EQ) {
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 [];
}, [feats, value]);
const styles = useStyles2(getStyles);
const LABEL_WIDTH = 10;
const onChangeProperty = useCallback(
(selection?: SelectableValue) => {
onChange({
...value,
check: {
...value.check!,
property: selection?.value,
},
});
},
[onChange, value]
);
const onChangeComparison = useCallback(
(selection: SelectableValue) => {
onChange({
...value,
check: {
...value.check!,
operation: selection.value ?? ComparisonOperation.EQ,
},
});
},
[onChange, value]
);
const onChangeValue = useCallback(
(selection?: SelectableValue) => {
onChange({
...value,
check: {
...value.check!,
value: selection?.value,
},
});
},
[onChange, value]
);
const onChangeNumericValue = useCallback(
(v?: number) => {
onChange({
...value,
check: {
...value.check!,
value: v!,
},
});
},
[onChange, value]
);
const onChangeStyle = useCallback(
(style?: StyleConfig) => {
onChange({ ...value, style });
},
[onChange, value]
);
const onDelete = useCallback(() => {
onChange(undefined);
}, [onChange]);
const check = value.check ?? DEFAULT_STYLE_RULE.check!;
const propv = getSelectionInfo(check.property, propertyOptions?.propertes);
const valuev = getSelectionInfo(check.value, uniqueSelectables);
return (
<div className={styles.rule}>
<InlineFieldRow className={styles.row}>
<InlineField label={t('geomap.style-rule-editor.label-rule', 'Rule')} labelWidth={LABEL_WIDTH} grow={true}>
<Select
placeholder={t('geomap.style-rule-editor.placeholder-feature-property', 'Feature property')}
value={propv.current}
options={propv.options}
onChange={onChangeProperty}
aria-label={t('geomap.style-rule-editor.aria-label-feature-property', 'Feature property')}
isClearable
allowCustomValue
/>
</InlineField>
<InlineField className={styles.inline}>
<Select
value={comparisonOperationOptions.find((v) => v.value === check.operation)}
options={comparisonOperationOptions}
onChange={onChangeComparison}
aria-label={t('geomap.style-rule-editor.aria-label-comparison-operator', 'Comparison operator')}
width={8}
/>
</InlineField>
<InlineField className={styles.inline} grow={true}>
<div className={styles.flexRow}>
{(check.operation === ComparisonOperation.EQ || check.operation === ComparisonOperation.NEQ) && (
<Select
placeholder={t('geomap.style-rule-editor.placeholder-value', 'value')}
value={valuev.current}
options={valuev.options}
onChange={onChangeValue}
aria-label={t('geomap.style-rule-editor.aria-label-comparison-value', 'Comparison value')}
isClearable
allowCustomValue
/>
)}
{check.operation !== ComparisonOperation.EQ && (
<NumberInput
key={`${check.property}/${check.operation}`}
value={!isNaN(Number(check.value)) ? Number(check.value) : 0}
placeholder={t('geomap.style-rule-editor.placeholder-numeric-value', 'Numeric value')}
onChange={onChangeNumericValue}
/>
)}
</div>
</InlineField>
<Button
size="md"
icon="trash-alt"
onClick={() => onDelete()}
variant="secondary"
aria-label={t('geomap.style-rule-editor.aria-label-delete-style-rule', 'Delete style rule')}
className={styles.button}
></Button>
</InlineFieldRow>
<div>
<StyleEditor
value={value.style ?? defaultStyleConfig}
context={context}
onChange={onChangeStyle}
item={
{
settings: {
simpleFixedValues: true,
layerInfo,
},
} as StandardEditorsRegistryItem
}
/>
</div>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
rule: css({
marginBottom: theme.spacing(1),
}),
row: css({
display: 'flex',
marginBottom: '4px',
}),
inline: css({
marginBottom: 0,
marginLeft: '4px',
}),
button: css({
marginLeft: '4px',
}),
flexRow: css({
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-start',
}),
});