mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* TraceQL - configurable static fields for new UI * TraceQL - filter out static fields from Tags section. Added tooltip to static fields * Add more units to duration validation. Improve duration field tooltip with accepted units * Better control of delete button on SearchField * Move new config behind feature toggle * Special title for intrinsic "name" * Fix tests * Move static fields not in the datasource to the Tags section * Start using the useAsync hook in the Tempo TraceQL configuration page to retrieve the tags and datasource * Fix tests * Fix test. Use useAsync to retrieve options in SearchField * Remove ability to set a default value in filter configuration. Removed type from filter, dynamic filters are now any filters not present in the datasource config * Updated the static filters tooltip * Replace useState + useEffect with useMemo for scopedTag
189 lines
6.3 KiB
TypeScript
189 lines
6.3 KiB
TypeScript
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;
|
|
}
|
|
const SearchField = ({
|
|
filter,
|
|
datasource,
|
|
updateFilter,
|
|
deleteFilter,
|
|
isTagsLoading,
|
|
tags,
|
|
setError,
|
|
hideScope,
|
|
hideTag,
|
|
hideValue,
|
|
allowDelete,
|
|
}: 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 await languageProvider.getOptionsV2(scopedTag);
|
|
} 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]);
|
|
|
|
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 (
|
|
<HorizontalGroup spacing={'none'} width={'auto'}>
|
|
{!hideScope && (
|
|
<Select
|
|
className={styles.dropdown}
|
|
inputId={`${filter.id}-scope`}
|
|
options={scopeOptions}
|
|
value={filter.scope}
|
|
onChange={(v) => {
|
|
updateFilter({ ...filter, scope: v?.value });
|
|
}}
|
|
placeholder="Select scope"
|
|
aria-label={`select ${filter.id} scope`}
|
|
/>
|
|
)}
|
|
{!hideTag && (
|
|
<Select
|
|
className={styles.dropdown}
|
|
inputId={`${filter.id}-tag`}
|
|
isLoading={isTagsLoading}
|
|
// Add the current tag to the list if it doesn't exist in the tags prop, otherwise the field will be empty even though the state has a value
|
|
options={(filter.tag !== undefined ? uniq([filter.tag, ...tags]) : tags).map((t) => ({
|
|
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}
|
|
/>
|
|
)}
|
|
<Select
|
|
className={styles.dropdown}
|
|
inputId={`${filter.id}-operator`}
|
|
options={operatorList.map(operatorSelectableValue)}
|
|
value={filter.operator}
|
|
onChange={(v) => {
|
|
updateFilter({ ...filter, operator: v?.value });
|
|
}}
|
|
isClearable={false}
|
|
aria-label={`select ${filter.id} operator`}
|
|
allowCustomValue={true}
|
|
width={8}
|
|
/>
|
|
{!hideValue && (
|
|
<Select
|
|
className={styles.dropdown}
|
|
inputId={`${filter.id}-value`}
|
|
isLoading={isLoadingValues}
|
|
options={options}
|
|
value={filter.value}
|
|
onChange={(val) => {
|
|
if (Array.isArray(val)) {
|
|
updateFilter({ ...filter, value: val.map((v) => v.value), valueType: val[0]?.type });
|
|
} else {
|
|
updateFilter({ ...filter, value: val?.value, valueType: val?.type });
|
|
}
|
|
}}
|
|
placeholder="Select value"
|
|
isClearable={false}
|
|
aria-label={`select ${filter.id} value`}
|
|
allowCustomValue={true}
|
|
isMulti
|
|
/>
|
|
)}
|
|
{allowDelete && (
|
|
<AccessoryButton
|
|
variant={'secondary'}
|
|
icon={'times'}
|
|
onClick={() => deleteFilter?.(filter)}
|
|
tooltip={'Remove tag'}
|
|
aria-label={`remove tag with ID ${filter.id}`}
|
|
/>
|
|
)}
|
|
</HorizontalGroup>
|
|
);
|
|
};
|
|
|
|
export default SearchField;
|