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