import { css } from '@emotion/css'; import { useCallback, useEffect, useState } from 'react'; import { CoreApp, GrafanaTheme2 } from '@grafana/data'; import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; import { config, FetchError, getTemplateSrv, reportInteraction } from '@grafana/runtime'; import { Alert, Button, HorizontalGroup, Select, useStyles2 } from '@grafana/ui'; import { RawQuery } from '../_importedDependencies/datasources/prometheus/RawQuery'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; import { TempoQueryBuilderOptions } from '../traceql/TempoQueryBuilderOptions'; import { traceqlGrammar } from '../traceql/traceql'; import { TempoQuery } from '../types'; import DurationInput from './DurationInput'; import { GroupByField } from './GroupByField'; import InlineSearchField from './InlineSearchField'; import SearchField from './SearchField'; import TagsInput from './TagsInput'; import { filterScopedTag, filterTitle, interpolateFilters, replaceAt } from './utils'; interface Props { datasource: TempoDatasource; query: TempoQuery; onChange: (value: TempoQuery) => void; onBlur?: () => void; onClearResults: () => void; app?: CoreApp; addVariablesToOptions?: boolean; } const hardCodedFilterIds = ['min-duration', 'max-duration', 'status']; const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVariablesToOptions = true }: Props) => { const styles = useStyles2(getStyles); const [alertText, setAlertText] = useState(); const [error, setError] = useState(null); const [isTagsLoading, setIsTagsLoading] = useState(true); const [traceQlQuery, setTraceQlQuery] = useState(''); const templateSrv = getTemplateSrv(); const updateFilter = useCallback( (s: TraceqlFilter) => { const copy = { ...query }; copy.filters ||= []; const indexOfFilter = copy.filters.findIndex((f) => f.id === s.id); if (indexOfFilter >= 0) { // update in place if the filter already exists, for consistency and to avoid UI bugs copy.filters = replaceAt(copy.filters, indexOfFilter, s); } else { copy.filters.push(s); } onChange(copy); }, [onChange, query] ); const deleteFilter = (s: TraceqlFilter) => { onChange({ ...query, filters: query.filters.filter((f) => f.id !== s.id) }); }; const templateVariables = getTemplateSrv().getVariables(); useEffect(() => { setTraceQlQuery(datasource.languageProvider.generateQueryFromFilters(interpolateFilters(query.filters || []))); }, [datasource.languageProvider, query, templateVariables]); const findFilter = useCallback((id: string) => query.filters?.find((f) => f.id === id), [query.filters]); useEffect(() => { const fetchTags = async () => { try { await datasource.languageProvider.start(); setIsTagsLoading(false); setAlertText(undefined); } catch (error) { if (error instanceof Error) { setAlertText(`Error: ${error.message}`); } } }; fetchTags(); }, [datasource, setAlertText]); useEffect(() => { // Initialize state with configured static filters that already have a value from the config datasource.search?.filters ?.filter((f) => f.value) .forEach((f) => { if (!findFilter(f.id)) { updateFilter(f); } }); }, [datasource.search?.filters, findFilter, updateFilter]); // filter out tags that already exist in the static fields const staticTags = datasource.search?.filters?.map((f) => f.tag) || []; staticTags.push('duration'); staticTags.push('traceDuration'); staticTags.push('span:duration'); staticTags.push('trace:duration'); staticTags.push('status'); staticTags.push('span:status'); // Dynamic filters are all filters that don't match the ID of a filter in the datasource configuration // The duration and status fields are a special case since its selector is hard-coded const dynamicFilters = (query.filters || []).filter( (f) => !hardCodedFilterIds.includes(f.id) && (datasource.search?.filters?.findIndex((sf) => sf.id === f.id) || 0) === -1 && f.id !== 'duration-type' ); // We use this function to generate queries without a specfic filter. // This is useful because we're sending the query to Tempo so it can return the attributes and values filtered down. // However, if we send the full query then we won't see more values for the filter we're trying to edit. // For example, if we already have a service.name value selected and try to add another one, we won't see the other // values if we send the full query since Tempo will only return the service.name that's already selected. const generateQueryWithoutFilter = (filter?: TraceqlFilter) => { if (!filter) { return traceQlQuery; } const filtersAfterRemoval = query.filters?.filter((f) => f.id !== filter.id) || []; return datasource.languageProvider.generateQueryFromFilters(interpolateFilters(filtersAfterRemoval || [])); }; return ( <>
{datasource.search?.filters?.map( (f) => f.tag && ( ) )}