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