1import React, { FC, useCallback, useMemo } from 'react';
2import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
3import { ComparisonOperation, FeatureStyleConfig } from '../types';
4import { Button, InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
5import { css } from '@emotion/css';
6import { StyleEditor } from '../layers/data/StyleEditor';
7import { defaultStyleConfig, StyleConfig } from '../style/types';
8import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
9import { Observable } from 'rxjs';
10import { useObservable } from 'react-use';
11import { getUniqueFeatureValues, LayerContentInfo } from '../utils/getFeatures';
12import { FeatureLike } from 'ol/Feature';
13import { getSelectionInfo } from '../utils/selection';
14import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
15
16export interface StyleRuleEditorSettings {
17  features: Observable<FeatureLike[]>;
18  layerInfo: Observable<LayerContentInfo>;
19}
20
21const comparators = [
22  { label: '==', value: ComparisonOperation.EQ },
23  { label: '!=', value: ComparisonOperation.NEQ },
24  { label: '>', value: ComparisonOperation.GT },
25  { label: '>=', value: ComparisonOperation.GTE },
26  { label: '<', value: ComparisonOperation.LT },
27  { label: '<=', value: ComparisonOperation.LTE },
28];
29
30export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, any, StyleRuleEditorSettings>> = (
31  props
32) => {
33  const { value, onChange, item, context } = props;
34  const settings: StyleRuleEditorSettings = item.settings;
35  const { features, layerInfo } = settings;
36
37  const propertyOptions = useObservable(layerInfo);
38  const feats = useObservable(features);
39
40  const uniqueSelectables = useMemo(() => {
41    const key = value?.check?.property;
42    if (key && feats && value.check?.operation === ComparisonOperation.EQ) {
43      return getUniqueFeatureValues(feats, key).map((v) => {
44        let newValue;
45        let isNewValueNumber = !isNaN(Number(v));
46
47        if (isNewValueNumber) {
48          newValue = {
49            value: Number(v),
50            label: v,
51          };
52        } else {
53          newValue = { value: v, label: v };
54        }
55
56        return newValue;
57      });
58    }
59    return [];
60  }, [feats, value]);
61
62  const styles = useStyles2(getStyles);
63
64  const LABEL_WIDTH = 10;
65
66  const onChangeProperty = useCallback(
67    (selection?: SelectableValue) => {
68      onChange({
69        ...value,
70        check: {
71          ...value.check!,
72          property: selection?.value,
73        },
74      });
75    },
76    [onChange, value]
77  );
78
79  const onChangeComparison = useCallback(
80    (selection: SelectableValue) => {
81      onChange({
82        ...value,
83        check: {
84          ...value.check!,
85          operation: selection.value ?? ComparisonOperation.EQ,
86        },
87      });
88    },
89    [onChange, value]
90  );
91
92  const onChangeValue = useCallback(
93    (selection?: SelectableValue) => {
94      onChange({
95        ...value,
96        check: {
97          ...value.check!,
98          value: selection?.value,
99        },
100      });
101    },
102    [onChange, value]
103  );
104
105  const onChangeNumericValue = useCallback(
106    (v?: number) => {
107      onChange({
108        ...value,
109        check: {
110          ...value.check!,
111          value: v!,
112        },
113      });
114    },
115    [onChange, value]
116  );
117
118  const onChangeStyle = useCallback(
119    (style?: StyleConfig) => {
120      onChange({ ...value, style });
121    },
122    [onChange, value]
123  );
124
125  const onDelete = useCallback(() => {
126    onChange(undefined);
127  }, [onChange]);
128
129  const check = value.check ?? DEFAULT_STYLE_RULE.check!;
130  const propv = getSelectionInfo(check.property, propertyOptions?.propertes);
131  const valuev = getSelectionInfo(check.value, uniqueSelectables);
132
133  return (
134    <div className={styles.rule}>
135      <InlineFieldRow className={styles.row}>
136        <InlineField label="Rule" labelWidth={LABEL_WIDTH} grow={true}>
137          <Select
138            menuShouldPortal
139            placeholder={'Feature property'}
140            value={propv.current}
141            options={propv.options}
142            onChange={onChangeProperty}
143            aria-label={'Feature property'}
144            isClearable
145            allowCustomValue
146          />
147        </InlineField>
148        <InlineField className={styles.inline}>
149          <Select
150            menuShouldPortal
151            value={comparators.find((v) => v.value === check.operation)}
152            options={comparators}
153            onChange={onChangeComparison}
154            aria-label={'Comparison operator'}
155            width={8}
156          />
157        </InlineField>
158        <InlineField className={styles.inline} grow={true}>
159          <>
160            {(check.operation === ComparisonOperation.EQ || check.operation === ComparisonOperation.NEQ) && (
161              <Select
162                menuShouldPortal
163                placeholder={'value'}
164                value={valuev.current}
165                options={valuev.options}
166                onChange={onChangeValue}
167                aria-label={'Comparison value'}
168                isClearable
169                allowCustomValue
170              />
171            )}
172            {check.operation !== ComparisonOperation.EQ && (
173              <NumberInput
174                key={`${check.property}/${check.operation}`}
175                value={!isNaN(Number(check.value)) ? Number(check.value) : 0}
176                placeholder="numeric value"
177                onChange={onChangeNumericValue}
178              />
179            )}
180          </>
181        </InlineField>
182        <Button
183          size="md"
184          icon="trash-alt"
185          onClick={() => onDelete()}
186          variant="secondary"
187          aria-label={'Delete style rule'}
188          className={styles.button}
189        ></Button>
190      </InlineFieldRow>
191      <div>
192        <StyleEditor
193          value={value.style ?? defaultStyleConfig}
194          context={context}
195          onChange={onChangeStyle}
196          item={
197            {
198              settings: {
199                simpleFixedValues: true,
200                layerInfo,
201              },
202            } as any
203          }
204        />
205      </div>
206    </div>
207  );
208};
209
210const getStyles = (theme: GrafanaTheme2) => ({
211  rule: css`
212    margin-bottom: ${theme.spacing(1)};
213  `,
214  row: css`
215    display: flex;
216    margin-bottom: 4px;
217  `,
218  inline: css`
219    margin-bottom: 0;
220    margin-left: 4px;
221  `,
222  button: css`
223    margin-left: 4px;
224  `,
225});
226