From d779dfb0a2eb00d0b699cc74b2fa1ed3239c7e90 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:24:58 +0100 Subject: [PATCH] Tempo: Select performance improvements (#91732) * Tempo select performance improvements * Update type * Tidy up and simplify * Update tagValueOptions * Update GroupBy options --- .../SearchTraceQLEditor/GroupByField.tsx | 32 ++++++--- .../tempo/SearchTraceQLEditor/SearchField.tsx | 71 ++++++++++++++----- 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx index dc1c3565d4f..220e2d810a4 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx @@ -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(''); 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) => { <> {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 || ''} /> diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx index 612c671d506..6fed3a0b35b 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx @@ -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(''); + const [tagValuesQuery, setTagValuesQuery] = useState(''); const updateOptions = async () => { try { @@ -127,13 +129,41 @@ const SearchField = ({ case 'float': operatorList = numberOperators; } + const operatorOptions = operatorList.map(operatorSelectableValue); - const tagOptions = (filter.tag !== undefined ? uniq([filter.tag, ...tags]) : tags).map((t) => ({ - label: t, - value: t, - })); + const formatTagOptions = (tags: string[], filterTag: string | undefined) => { + return (filterTag !== undefined ? uniq([filterTag, ...tags]) : tags).map((t) => ({ label: t, value: t })); + }; - const operatorOptions = operatorList.map(operatorSelectableValue); + 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 ( <> @@ -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;