diff --git a/.betterer.results b/.betterer.results index d305a5130af..7436903ef0d 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5638,12 +5638,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], - "public/app/plugins/datasource/tempo/_importedDependencies/store.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], "public/app/plugins/datasource/tempo/_importedDependencies/test/helpers/createFetchResponse.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] diff --git a/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx b/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx index cabc3e3c493..8a7003a576c 100644 --- a/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx @@ -58,5 +58,17 @@ export const TemporaryAlert = (props: AlertProps) => { } }, [props.severity, props.text]); - return <>{visible && }; + return ( + <> + {visible && ( + setVisible(false)} + severity={props.severity} + title={props.text} + /> + )} + + ); }; diff --git a/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx b/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx index ae78da4318f..1653cc04de7 100644 --- a/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx +++ b/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx @@ -2,12 +2,10 @@ import { css } from '@emotion/css'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { GrafanaTheme2, isValidGoDuration, SelectableValue, toOption } from '@grafana/data'; +import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; import { FetchError, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime'; import { InlineFieldRow, InlineField, Input, Alert, useStyles2, fuzzyMatch, Select } from '@grafana/ui'; -import { notifyApp } from '../_importedDependencies/actions/appNotification'; -import { createErrorNotification } from '../_importedDependencies/core/appNotification'; -import { dispatch } from '../_importedDependencies/store'; import { DEFAULT_LIMIT, TempoDatasource } from '../datasource'; import TempoLanguageProvider from '../language_provider'; import { TempoQuery } from '../types'; @@ -26,6 +24,7 @@ const durationPlaceholder = 'e.g. 1.2s, 100ms'; const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props) => { const styles = useStyles2(getStyles); + const [alertText, setAlertText] = useState(); const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]); const [serviceOptions, setServiceOptions] = useState>>(); const [spanOptions, setSpanOptions] = useState>>(); @@ -47,19 +46,21 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props try { const options = await languageProvider.getOptionsV1(lpName); const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false)); + setAlertText(undefined); + setError(null); return filteredOptions; } catch (error) { if (isFetchError(error) && error?.status === 404) { setError(error); } else if (error instanceof Error) { - dispatch(notifyApp(createErrorNotification('Error', error))); + setAlertText(`Error: ${error.message}`); } return []; } finally { setIsLoading((prevValue) => ({ ...prevValue, [name]: false })); } }, - [languageProvider] + [languageProvider, setAlertText] ); useEffect(() => { @@ -74,17 +75,19 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props spans.push(toOption(query.spanName)); } setSpanOptions(spans); + setAlertText(undefined); + setError(null); } 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))); + setAlertText(`Error: ${error.message}`); } } }; fetchOptions(); - }, [languageProvider, loadOptions, query.serviceName, query.spanName]); + }, [languageProvider, loadOptions, query.serviceName, query.spanName, setAlertText]); const onKeyDown = (keyEvent: React.KeyboardEvent) => { if (keyEvent.key === 'Enter' && (keyEvent.shiftKey || keyEvent.ctrlKey)) { @@ -255,6 +258,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props configure it in the datasource settings. ) : null} + {alertText && } ); }; diff --git a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx b/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx index 3869c3fcb3c..9acc12b2899 100644 --- a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx +++ b/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx @@ -1,12 +1,10 @@ import { css } from '@emotion/css'; -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; import { CodeEditor, Monaco, monacoTypes, useTheme2 } from '@grafana/ui'; -import { notifyApp } from '../../_importedDependencies/actions/appNotification'; -import { createErrorNotification } from '../../_importedDependencies/core/appNotification'; -import { dispatch } from '../../_importedDependencies/store'; import { TempoDatasource } from '../../datasource'; import { CompletionProvider } from './autocomplete'; @@ -21,40 +19,44 @@ interface Props { } export function TagsField(props: Props) { + const [alertText, setAlertText] = useState(); const { onChange, onBlur, placeholder } = props; - const setupAutocompleteFn = useAutocomplete(props.datasource); + const setupAutocompleteFn = useAutocomplete(props.datasource, setAlertText); const theme = useTheme2(); const styles = getStyles(theme, placeholder); return ( - { - setupAutocompleteFn(editor, monaco); - setupPlaceholder(editor, monaco, styles); - setupAutoSize(editor); - }} - /> + <> + { + setupAutocompleteFn(editor, monaco); + setupPlaceholder(editor, monaco, styles); + setupAutoSize(editor); + }} + /> + {alertText && } + ); } @@ -103,9 +105,10 @@ function setupAutoSize(editor: monacoTypes.editor.IStandaloneCodeEditor) { /** * Hook that returns function that will set up monaco autocomplete for the label selector - * @param datasource + * @param datasource the Tempo datasource instance + * @param setAlertText setter for the alert text */ -function useAutocomplete(datasource: TempoDatasource) { +function useAutocomplete(datasource: TempoDatasource, setAlertText: (text?: string) => void) { // We need the provider ref so we can pass it the label/values data later. This is because we run the call for the // values here but there is additional setup needed for the provider later on. We could run the getSeries() in the // returned function but that is run after the monaco is mounted so would delay the request a bit when it does not @@ -118,14 +121,15 @@ function useAutocomplete(datasource: TempoDatasource) { const fetchTags = async () => { try { await datasource.languageProvider.start(); + setAlertText(undefined); } catch (error) { if (error instanceof Error) { - dispatch(notifyApp(createErrorNotification('Error', error))); + setAlertText(`Error: ${error.message}`); } } }; fetchTags(); - }, [datasource]); + }, [datasource, setAlertText]); const autocompleteDisposeFun = useRef<(() => void) | null>(null); useEffect(() => { diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx index e81d0208cad..42a4935371f 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx @@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { LanguageProvider } from '@grafana/data'; -import { FetchError } from '@grafana/runtime'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; @@ -290,9 +289,7 @@ const renderSearchField = ( datasource={datasource} updateFilter={updateFilter} filter={filter} - setError={function (error: FetchError): void { - throw error; - }} + setError={() => {}} tags={tags || []} hideTag={hideTag} query={'{}'} diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx index 68cc5a2676d..eaaeda32221 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx @@ -4,12 +4,10 @@ import React, { useState, useEffect, 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, HorizontalGroup, useStyles2 } from '@grafana/ui'; -import { notifyApp } from '../_importedDependencies/actions/appNotification'; -import { createErrorNotification } from '../_importedDependencies/core/appNotification'; -import { dispatch } from '../_importedDependencies/store'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; import { operators as allOperators, stringOperators, numberOperators, keywordOperators } from '../traceql/traceql'; @@ -26,7 +24,8 @@ interface Props { filter: TraceqlFilter; datasource: TempoDatasource; updateFilter: (f: TraceqlFilter) => void; - setError: (error: FetchError) => void; + deleteFilter?: (f: TraceqlFilter) => void; + setError: (error: FetchError | null) => void; isTagsLoading?: boolean; tags: string[]; hideScope?: boolean; @@ -51,6 +50,7 @@ const SearchField = ({ allowCustomValue = true, }: Props) => { const styles = useStyles2(getStyles); + const [alertText, setAlertText] = useState(); 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 @@ -60,13 +60,16 @@ const SearchField = ({ const updateOptions = async () => { try { - return filter.tag ? await datasource.languageProvider.getOptionsV2(scopedTag, query) : []; + 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) { - dispatch(notifyApp(createErrorNotification('Error', error))); + setAlertText(`Error: ${error.message}`); } } return []; @@ -135,78 +138,85 @@ const SearchField = ({ }; return ( - - {!hideScope && ( + <> + + {!hideScope && ( + ({ + label: t, + value: t, + })) + )} + value={filter.tag} + onChange={(v) => { + updateFilter({ ...filter, tag: v?.value, value: [] }); + }} + placeholder="Select tag" + isClearable + aria-label={`select ${filter.id} tag`} + allowCustomValue={true} + /> + )} ({ - label: t, - value: t, - })) - )} - value={filter.tag} - onChange={(v) => { - updateFilter({ ...filter, tag: v?.value, value: [] }); - }} - placeholder="Select tag" - isClearable - aria-label={`select ${filter.id} tag`} + isClearable={false} + aria-label={`select ${filter.id} operator`} allowCustomValue={true} + width={8} /> - )} - { - 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 - /> - )} - + {!hideValue && ( +