Tempo: Select performance improvements (#91732)

* Tempo select performance improvements

* Update type

* Tidy up and simplify

* Update tagValueOptions

* Update GroupBy options
This commit is contained in:
Joey 2024-08-13 15:24:58 +01:00 committed by GitHub
parent d72846790e
commit d779dfb0a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 80 additions and 25 deletions

View File

@ -1,17 +1,17 @@
import { css } from '@emotion/css';
import { useEffect } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data';
import { AccessoryButton } from '@grafana/experimental';
import { HorizontalGroup, Select, useStyles2 } from '@grafana/ui';
import { HorizontalGroup, InputActionMeta, Select, useStyles2 } from '@grafana/ui';
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
import { TempoDatasource } from '../datasource';
import { TempoQuery } from '../types';
import InlineSearchField from './InlineSearchField';
import { withTemplateVariableOptions } from './SearchField';
import { maxOptions, withTemplateVariableOptions } from './SearchField';
import { replaceAt } from './utils';
interface Props {
@ -26,6 +26,7 @@ export const GroupByField = (props: Props) => {
const { datasource, onChange, query, isTagsLoading, addVariablesToOptions } = props;
const styles = useStyles2(getStyles);
const generateId = () => uuidv4().slice(0, 8);
const [tagQuery, setTagQuery] = useState<string>('');
useEffect(() => {
if (!query.groupBy || query.groupBy.length === 0) {
@ -41,9 +42,18 @@ export const GroupByField = (props: Props) => {
}
}, [onChange, query]);
const getTags = (f: TraceqlFilter) => {
return datasource!.languageProvider.getMetricsSummaryTags(f.scope);
};
const tagOptions = useMemo(
() => (f: TraceqlFilter) => {
const tags = datasource!.languageProvider.getMetricsSummaryTags(f.scope);
if (tagQuery.length === 0) {
return tags.slice(0, maxOptions);
}
const queryLowerCase = tagQuery.toLowerCase();
return tags.filter((tag) => tag.toLowerCase().includes(queryLowerCase)).slice(0, maxOptions);
},
[datasource, tagQuery]
);
const addFilter = () => {
updateFilter({
@ -74,8 +84,8 @@ export const GroupByField = (props: Props) => {
<InlineSearchField label="Aggregate by" tooltip="Select one or more tags to see the metrics summary.">
<>
{query.groupBy?.map((f, i) => {
const tags = getTags(f)
?.concat(f.tag !== undefined && !getTags(f)?.includes(f.tag) ? [f.tag] : [])
const tags = tagOptions(f)
?.concat(f.tag !== undefined && !tagOptions(f)?.includes(f.tag) ? [f.tag] : [])
.map((t) => ({
label: t,
value: t,
@ -102,6 +112,12 @@ export const GroupByField = (props: Props) => {
updateFilter({ ...f, tag: v?.value });
}}
options={addVariablesToOptions ? withTemplateVariableOptions(tags) : tags}
onInputChange={(value: string, { action }: InputActionMeta) => {
if (action === 'input-change') {
setTagQuery(value);
}
}}
onCloseMenu={() => setTagQuery('')}
placeholder="Select tag"
value={f.tag || ''}
/>

View File

@ -6,7 +6,7 @@ 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, HorizontalGroup, useStyles2 } from '@grafana/ui';
import { Select, HorizontalGroup, useStyles2, InputActionMeta } from '@grafana/ui';
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
import { TempoDatasource } from '../datasource';
@ -59,6 +59,8 @@ const SearchField = ({
// 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 [tagQuery, setTagQuery] = useState<string>('');
const [tagValuesQuery, setTagValuesQuery] = useState<string>('');
const updateOptions = async () => {
try {
@ -127,14 +129,42 @@ const SearchField = ({
case 'float':
operatorList = numberOperators;
}
const tagOptions = (filter.tag !== undefined ? uniq([filter.tag, ...tags]) : tags).map((t) => ({
label: t,
value: t,
}));
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, maxOptions), filter.tag);
}
const queryLowerCase = tagQuery.toLowerCase();
const filterdOptions = tags.filter((tag) => tag.toLowerCase().includes(queryLowerCase)).slice(0, maxOptions);
return formatTagOptions(filterdOptions, filter.tag);
}, [filter.tag, tagQuery, tags]);
const tagValueOptions = useMemo(() => {
if (!options) {
return;
}
if (tagValuesQuery.length === 0) {
return options.slice(0, maxOptions);
}
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, maxOptions);
}, [tagValuesQuery, options]);
return (
<>
<HorizontalGroup spacing={'none'} width={'auto'}>
@ -144,9 +174,7 @@ const SearchField = ({
inputId={`${filter.id}-scope`}
options={addVariablesToOptions ? withTemplateVariableOptions(scopeOptions) : scopeOptions}
value={filter.scope}
onChange={(v) => {
updateFilter({ ...filter, scope: v?.value });
}}
onChange={(v) => updateFilter({ ...filter, scope: v?.value })}
placeholder="Select scope"
aria-label={`select ${filter.id} scope`}
/>
@ -158,10 +186,14 @@ const SearchField = ({
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}
value={filter.tag}
onChange={(v) => {
updateFilter({ ...filter, tag: v?.value, value: [] });
onInputChange={(value: string, { action }: InputActionMeta) => {
if (action === 'input-change') {
setTagQuery(value);
}
}}
onCloseMenu={() => setTagQuery('')}
onChange={(v) => updateFilter({ ...filter, tag: v?.value, value: [] })}
value={filter.tag}
placeholder="Select tag"
isClearable
aria-label={`select ${filter.id} tag`}
@ -174,9 +206,7 @@ const SearchField = ({
inputId={`${filter.id}-operator`}
options={addVariablesToOptions ? withTemplateVariableOptions(operatorOptions) : operatorOptions}
value={filter.operator}
onChange={(v) => {
updateFilter({ ...filter, operator: v?.value });
}}
onChange={(v) => updateFilter({ ...filter, operator: v?.value })}
isClearable={false}
aria-label={`select ${filter.id} operator`}
allowCustomValue={true}
@ -193,8 +223,14 @@ const SearchField = ({
className={styles.dropdown}
inputId={`${filter.id}-value`}
isLoading={isLoadingValues}
options={addVariablesToOptions ? withTemplateVariableOptions(options) : options}
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({
@ -231,4 +267,7 @@ export const withTemplateVariableOptions = (options: SelectableValue[] | undefin
return [...(options || []), ...templateVariables.map((v) => ({ label: `$${v.name}`, value: `$${v.name}` }))];
};
// Limit maximum options in select dropdowns for performance reasons
export const maxOptions = 10000;
export default SearchField;