mirror of https://github.com/grafana/grafana.git
229 lines
6.8 KiB
TypeScript
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',
|
|
}),
|
|
});
|