1import React, { FunctionComponent, useCallback, useMemo } from 'react'; 2import { flatten } from 'lodash'; 3 4import { SelectableValue, toOption } from '@grafana/data'; 5import { CustomControlProps } from '@grafana/ui/src/components/Select/types'; 6import { Button, HorizontalGroup, Select, VerticalGroup } from '@grafana/ui'; 7import { labelsToGroupedOptions, stringArrayToFilters } from '../functions'; 8import { Filter } from '../types'; 9import { SELECT_WIDTH } from '../constants'; 10import { QueryEditorRow } from '.'; 11 12export interface Props { 13 labels: { [key: string]: string[] }; 14 filters: string[]; 15 onChange: (filters: string[]) => void; 16 variableOptionGroup: SelectableValue<string>; 17} 18 19const operators = ['=', '!=', '=~', '!=~']; 20 21const FilterButton = React.forwardRef<HTMLButtonElement, CustomControlProps<string>>( 22 ({ value, isOpen, invalid, ...rest }, ref) => { 23 return <Button {...rest} ref={ref} variant="secondary" icon="plus"></Button>; 24 } 25); 26FilterButton.displayName = 'FilterButton'; 27 28const OperatorButton = React.forwardRef<HTMLButtonElement, CustomControlProps<string>>(({ value, ...rest }, ref) => { 29 return ( 30 <Button {...rest} ref={ref} variant="secondary"> 31 <span className="query-segment-operator">{value?.label}</span> 32 </Button> 33 ); 34}); 35OperatorButton.displayName = 'OperatorButton'; 36 37export const LabelFilter: FunctionComponent<Props> = ({ 38 labels = {}, 39 filters: filterArray, 40 onChange, 41 variableOptionGroup, 42}) => { 43 const filters = useMemo(() => stringArrayToFilters(filterArray), [filterArray]); 44 const options = useMemo(() => [variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))], [ 45 labels, 46 variableOptionGroup, 47 ]); 48 49 const filtersToStringArray = useCallback((filters: Filter[]) => { 50 const strArr = flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition!])); 51 return strArr.slice(0, strArr.length - 1); 52 }, []); 53 54 const AddFilter = () => { 55 return ( 56 <Select 57 menuShouldPortal 58 allowCustomValue 59 options={[variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))]} 60 onChange={({ value: key = '' }) => 61 onChange(filtersToStringArray([...filters, { key, operator: '=', condition: 'AND', value: '' }])) 62 } 63 menuPlacement="bottom" 64 renderControl={FilterButton} 65 /> 66 ); 67 }; 68 69 return ( 70 <QueryEditorRow 71 label="Filter" 72 tooltip={ 73 'To reduce the amount of data charted, apply a filter. A filter has three components: a label, a comparison, and a value. The comparison can be an equality, inequality, or regular expression.' 74 } 75 noFillEnd={filters.length > 1} 76 > 77 <VerticalGroup spacing="xs" width="auto"> 78 {filters.map(({ key, operator, value, condition }, index) => { 79 // Add the current key and value as options if they are manually entered 80 const keyPresent = options.some((op) => { 81 if (op.options) { 82 return options.some((opp) => opp.label === key); 83 } 84 return op.label === key; 85 }); 86 if (!keyPresent) { 87 options.push({ label: key, value: key }); 88 } 89 90 const valueOptions = labels.hasOwnProperty(key) 91 ? [variableOptionGroup, ...labels[key].map(toOption)] 92 : [variableOptionGroup]; 93 const valuePresent = valueOptions.some((op) => { 94 return op.label === value; 95 }); 96 if (!valuePresent) { 97 valueOptions.push({ label: value, value }); 98 } 99 100 return ( 101 <HorizontalGroup key={index} spacing="xs" width="auto"> 102 <Select 103 menuShouldPortal 104 width={SELECT_WIDTH} 105 allowCustomValue 106 formatCreateLabel={(v) => `Use label key: ${v}`} 107 value={key} 108 options={options} 109 onChange={({ value: key = '' }) => { 110 onChange( 111 filtersToStringArray( 112 filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f)) 113 ) 114 ); 115 }} 116 /> 117 <Select 118 menuShouldPortal 119 value={operator} 120 options={operators.map(toOption)} 121 onChange={({ value: operator = '=' }) => 122 onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f)))) 123 } 124 menuPlacement="bottom" 125 renderControl={OperatorButton} 126 /> 127 <Select 128 menuShouldPortal 129 width={SELECT_WIDTH} 130 formatCreateLabel={(v) => `Use label value: ${v}`} 131 allowCustomValue 132 value={value} 133 placeholder="add filter value" 134 options={valueOptions} 135 onChange={({ value = '' }) => 136 onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f)))) 137 } 138 /> 139 <Button 140 variant="secondary" 141 size="md" 142 icon="trash-alt" 143 aria-label="Remove" 144 onClick={() => onChange(filtersToStringArray(filters.filter((_, i) => i !== index)))} 145 ></Button> 146 {index + 1 === filters.length && Object.values(filters).every(({ value }) => value) && <AddFilter />} 147 </HorizontalGroup> 148 ); 149 })} 150 {!filters.length && <AddFilter />} 151 </VerticalGroup> 152 </QueryEditorRow> 153 ); 154}; 155