import { css } from '@emotion/css'; import { uniq } from 'lodash'; import React, { useState, useEffect, useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { AccessoryButton } from '@grafana/experimental'; import { FetchError, isFetchError } from '@grafana/runtime'; import { Select, HorizontalGroup, useStyles2 } from '@grafana/ui'; import { createErrorNotification } from '../../../../core/copy/appNotification'; import { notifyApp } from '../../../../core/reducers/appNotification'; import { dispatch } from '../../../../store/store'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; import TempoLanguageProvider from '../language_provider'; import { operators as allOperators, stringOperators, numberOperators } from '../traceql/traceql'; import { filterScopedTag, operatorSelectableValue } from './utils'; const getStyles = () => ({ dropdown: css` box-shadow: none; `, }); interface Props { filter: TraceqlFilter; datasource: TempoDatasource; updateFilter: (f: TraceqlFilter) => void; deleteFilter?: (f: TraceqlFilter) => void; setError: (error: FetchError) => void; isTagsLoading?: boolean; tags: string[]; hideScope?: boolean; hideTag?: boolean; hideValue?: boolean; allowDelete?: boolean; query: string; } const SearchField = ({ filter, datasource, updateFilter, deleteFilter, isTagsLoading, tags, setError, hideScope, hideTag, hideValue, allowDelete, query, }: Props) => { const styles = useStyles2(getStyles); const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]); const scopedTag = useMemo(() => filterScopedTag(filter), [filter]); // We automatically change the operator to the regex op when users select 2 or more values // However, they expect this to be automatically rolled back to the previous operator once // there's only one value selected, so we store the previous operator and value const [prevOperator, setPrevOperator] = useState(filter.operator); const [prevValue, setPrevValue] = useState(filter.value); const updateOptions = async () => { try { return filter.tag ? await languageProvider.getOptionsV2(scopedTag, query) : []; } catch (error) { // Display message if Tempo is connected but search 404's if (isFetchError(error) && error?.status === 404) { setError(error); } else if (error instanceof Error) { dispatch(notifyApp(createErrorNotification('Error', error))); } } return []; }; const { loading: isLoadingValues, value: options } = useAsync(updateOptions, [ scopedTag, languageProvider, setError, query, ]); useEffect(() => { if (Array.isArray(filter.value) && filter.value.length > 1 && filter.operator !== '=~') { setPrevOperator(filter.operator); updateFilter({ ...filter, operator: '=~' }); } if (Array.isArray(filter.value) && filter.value.length <= 1 && (prevValue?.length || 0) > 1) { updateFilter({ ...filter, operator: prevOperator, value: filter.value[0] }); } }, [prevValue, prevOperator, updateFilter, filter]); useEffect(() => { setPrevValue(filter.value); }, [filter.value]); const scopeOptions = Object.values(TraceqlSearchScope).map((t) => ({ label: t, value: t })); // If all values have type string or int/float use a focused list of operators instead of all operators const optionsOfFirstType = options?.filter((o) => o.type === options[0]?.type); const uniqueOptionType = options?.length === optionsOfFirstType?.length ? options?.[0]?.type : undefined; let operatorList = allOperators; switch (uniqueOptionType) { case 'string': operatorList = stringOperators; break; case 'int': case 'float': operatorList = numberOperators; } return ( {!hideScope && ( ({ label: t, value: t, }))} value={filter.tag} onChange={(v) => { updateFilter({ ...filter, tag: v?.value }); }} placeholder="Select tag" isClearable aria-label={`select ${filter.id} tag`} allowCustomValue={true} /> )} { if (Array.isArray(val)) { updateFilter({ ...filter, value: val.map((v) => v.value), valueType: val[0]?.type || uniqueOptionType }); } else { updateFilter({ ...filter, value: val?.value, valueType: val?.type || uniqueOptionType }); } }} placeholder="Select value" isClearable={false} aria-label={`select ${filter.id} value`} allowCustomValue={true} isMulti allowCreateWhileLoading /> )} {allowDelete && ( deleteFilter?.(filter)} tooltip={'Remove tag'} aria-label={`remove tag with ID ${filter.id}`} /> )} ); }; export default SearchField;