The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx

257 lines
8.9 KiB

import { css } from '@emotion/css';
import { uniq } from 'lodash';
import { useState, useMemo } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { SelectableValue } from '@grafana/data';
import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
import { FetchError, getTemplateSrv, isFetchError } from '@grafana/runtime';
import { Select, Stack, useStyles2, InputActionMeta } from '@grafana/ui';
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
import { TempoDatasource } from '../datasource';
import { OPTIONS_LIMIT } from '../language_provider';
import { operators as allOperators, stringOperators, numberOperators, keywordOperators } from '../traceql/traceql';
import { filterScopedTag, operatorSelectableValue } from './utils';
interface Props {
filter: TraceqlFilter;
datasource: TempoDatasource;
updateFilter: (f: TraceqlFilter) => void;
deleteFilter?: (f: TraceqlFilter) => void;
setError: (error: FetchError | null) => void;
isTagsLoading?: boolean;
tags: string[];
hideScope?: boolean;
hideTag?: boolean;
hideValue?: boolean;
query: string;
isMulti?: boolean;
allowCustomValue?: boolean;
addVariablesToOptions?: boolean;
}
const SearchField = ({
filter,
datasource,
updateFilter,
isTagsLoading,
tags,
setError,
hideScope,
hideTag,
hideValue,
query,
addVariablesToOptions,
isMulti = true,
allowCustomValue = true,
}: Props) => {
const styles = useStyles2(getStyles);
const [alertText, setAlertText] = useState<string>();
const scopedTag = useMemo(
() => filterScopedTag(filter, datasource.languageProvider),
[datasource.languageProvider, filter]
);
const [tagQuery, setTagQuery] = useState<string>('');
const [tagValuesQuery, setTagValuesQuery] = useState<string>('');
const updateOptions = async () => {
try {
const result = filter.tag ? await datasource.languageProvider.getOptionsV2(scopedTag, query) : [];
setAlertText(undefined);
setError(null);
return result;
} 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) {
setAlertText(`Error: ${error.message}`);
}
}
return [];
};
const { loading: isLoadingValues, value: options } = useAsync(updateOptions, [
scopedTag,
datasource.languageProvider,
setError,
query,
]);
// Add selected option if it doesn't exist in the current list of options
if (filter.value && !Array.isArray(filter.value) && options && !options.find((o) => o.value === filter.value)) {
options.push({ label: filter.value.toString(), value: filter.value.toString(), type: filter.valueType });
}
const scopeOptions = Object.values(TraceqlSearchScope)
.filter((s) => {
// only add scope if it has tags
return datasource.languageProvider.getTags(s).length > 0;
})
.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 'keyword':
operatorList = keywordOperators;
break;
case 'string':
operatorList = stringOperators;
break;
case 'int':
case 'float':
operatorList = numberOperators;
}
const operatorOptions = operatorList.map(operatorSelectableValue);
const formatTagOptions = (tags: string[], filterTag: string | undefined) => {
return (filterTag !== undefined ? uniq([filterTag, ...tags]) : tags).map((t) => ({ label: t, value: t }));
};
const tagOptions = useMemo(() => {
if (tagQuery.length === 0) {
return formatTagOptions(tags.slice(0, OPTIONS_LIMIT), filter.tag);
}
const queryLowerCase = tagQuery.toLowerCase();
const filterdOptions = tags.filter((tag) => tag.toLowerCase().includes(queryLowerCase)).slice(0, OPTIONS_LIMIT);
return formatTagOptions(filterdOptions, filter.tag);
}, [filter.tag, tagQuery, tags]);
const tagValueOptions = useMemo(() => {
if (!options) {
return;
}
if (tagValuesQuery.length === 0) {
return options.slice(0, OPTIONS_LIMIT);
}
const queryLowerCase = tagValuesQuery.toLowerCase();
return options
.filter((tag) => {
if (tag.value && tag.value.length > 0) {
return tag.value.toLowerCase().includes(queryLowerCase);
}
return false;
})
.slice(0, OPTIONS_LIMIT);
}, [tagValuesQuery, options]);
return (
<>
<Stack gap={0} width="auto">
{!hideScope && (
<Select
width="auto"
className={styles.dropdown}
inputId={`${filter.id}-scope`}
options={addVariablesToOptions ? withTemplateVariableOptions(scopeOptions) : scopeOptions}
value={filter.scope}
onChange={(v) => updateFilter({ ...filter, scope: v?.value, tag: undefined, value: [] })}
placeholder="Select scope"
aria-label={`select ${filter.id} scope`}
/>
)}
{!hideTag && (
<Select
width="auto"
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={addVariablesToOptions ? withTemplateVariableOptions(tagOptions) : tagOptions}
onInputChange={(value: string, { action }: InputActionMeta) => {
if (action === 'input-change') {
setTagQuery(value);
}
}}
onCloseMenu={() => setTagQuery('')}
onChange={(v) => updateFilter({ ...filter, tag: v?.value, value: [] })}
value={filter.tag}
key={filter.tag}
placeholder="Select tag"
isClearable
aria-label={`select ${filter.id} tag`}
allowCustomValue
virtualized
/>
)}
<Select
className={styles.dropdown}
inputId={`${filter.id}-operator`}
options={addVariablesToOptions ? withTemplateVariableOptions(operatorOptions) : operatorOptions}
value={filter.operator}
onChange={(v) => updateFilter({ ...filter, operator: v?.value })}
isClearable={false}
aria-label={`select ${filter.id} operator`}
allowCustomValue={true}
width={8}
/>
{!hideValue && (
<Select
/**
* Trace cardinality means we need to use the virtualized variant of the Select component.
* For example the number of span names being returned can easily reach 10s of thousands,
* which is enough to cause a user's web browser to seize up
*/
width="auto"
virtualized
className={styles.dropdown}
inputId={`${filter.id}-value`}
isLoading={isLoadingValues}
options={addVariablesToOptions ? withTemplateVariableOptions(tagValueOptions) : tagValueOptions}
value={filter.value}
onInputChange={(value: string, { action }: InputActionMeta) => {
if (action === 'input-change') {
setTagValuesQuery(value);
}
}}
onCloseMenu={() => setTagValuesQuery('')}
onChange={(val) => {
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={true}
aria-label={`select ${filter.id} value`}
allowCustomValue={allowCustomValue}
isMulti={isMulti}
allowCreateWhileLoading
/>
)}
</Stack>
{alertText && <TemporaryAlert severity="error" text={alertText} />}
</>
);
};
export default SearchField;
/**
* Add to a list of options the current template variables.
*
* @param options a list of options
* @returns the list of given options plus the template variables
*/
export const withTemplateVariableOptions = (options: SelectableValue[] | undefined) => {
const templateVariables = getTemplateSrv().getVariables();
return [...(options || []), ...templateVariables.map((v) => ({ label: `$${v.name}`, value: `$${v.name}` }))];
};
const getStyles = () => ({
dropdown: css({
boxShadow: 'none',
}),
});