mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tempo: Remove duplicated code (#81476)
This commit is contained in:
parent
80d6bf6da0
commit
2fa4ac2a73
@ -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.", "1"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
[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": [
|
"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.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||||
|
@ -58,5 +58,17 @@ export const TemporaryAlert = (props: AlertProps) => {
|
|||||||
}
|
}
|
||||||
}, [props.severity, props.text]);
|
}, [props.severity, props.text]);
|
||||||
|
|
||||||
return <>{visible && <Alert className={style} elevated={true} title={props.text} severity={props.severity} />}</>;
|
return (
|
||||||
|
<>
|
||||||
|
{visible && (
|
||||||
|
<Alert
|
||||||
|
className={style}
|
||||||
|
elevated={true}
|
||||||
|
onRemove={() => setVisible(false)}
|
||||||
|
severity={props.severity}
|
||||||
|
title={props.text}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,12 +2,10 @@ import { css } from '@emotion/css';
|
|||||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, isValidGoDuration, SelectableValue, toOption } from '@grafana/data';
|
import { GrafanaTheme2, isValidGoDuration, SelectableValue, toOption } from '@grafana/data';
|
||||||
|
import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
|
||||||
import { FetchError, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime';
|
import { FetchError, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime';
|
||||||
import { InlineFieldRow, InlineField, Input, Alert, useStyles2, fuzzyMatch, Select } from '@grafana/ui';
|
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 { DEFAULT_LIMIT, TempoDatasource } from '../datasource';
|
||||||
import TempoLanguageProvider from '../language_provider';
|
import TempoLanguageProvider from '../language_provider';
|
||||||
import { TempoQuery } from '../types';
|
import { TempoQuery } from '../types';
|
||||||
@ -26,6 +24,7 @@ const durationPlaceholder = 'e.g. 1.2s, 100ms';
|
|||||||
|
|
||||||
const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props) => {
|
const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const [alertText, setAlertText] = useState<string>();
|
||||||
const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]);
|
const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]);
|
||||||
const [serviceOptions, setServiceOptions] = useState<Array<SelectableValue<string>>>();
|
const [serviceOptions, setServiceOptions] = useState<Array<SelectableValue<string>>>();
|
||||||
const [spanOptions, setSpanOptions] = useState<Array<SelectableValue<string>>>();
|
const [spanOptions, setSpanOptions] = useState<Array<SelectableValue<string>>>();
|
||||||
@ -47,19 +46,21 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
|
|||||||
try {
|
try {
|
||||||
const options = await languageProvider.getOptionsV1(lpName);
|
const options = await languageProvider.getOptionsV1(lpName);
|
||||||
const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false));
|
const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false));
|
||||||
|
setAlertText(undefined);
|
||||||
|
setError(null);
|
||||||
return filteredOptions;
|
return filteredOptions;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isFetchError(error) && error?.status === 404) {
|
if (isFetchError(error) && error?.status === 404) {
|
||||||
setError(error);
|
setError(error);
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
setAlertText(`Error: ${error.message}`);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading((prevValue) => ({ ...prevValue, [name]: false }));
|
setIsLoading((prevValue) => ({ ...prevValue, [name]: false }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[languageProvider]
|
[languageProvider, setAlertText]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -74,17 +75,19 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
|
|||||||
spans.push(toOption(query.spanName));
|
spans.push(toOption(query.spanName));
|
||||||
}
|
}
|
||||||
setSpanOptions(spans);
|
setSpanOptions(spans);
|
||||||
|
setAlertText(undefined);
|
||||||
|
setError(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Display message if Tempo is connected but search 404's
|
// Display message if Tempo is connected but search 404's
|
||||||
if (isFetchError(error) && error?.status === 404) {
|
if (isFetchError(error) && error?.status === 404) {
|
||||||
setError(error);
|
setError(error);
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
setAlertText(`Error: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchOptions();
|
fetchOptions();
|
||||||
}, [languageProvider, loadOptions, query.serviceName, query.spanName]);
|
}, [languageProvider, loadOptions, query.serviceName, query.spanName, setAlertText]);
|
||||||
|
|
||||||
const onKeyDown = (keyEvent: React.KeyboardEvent) => {
|
const onKeyDown = (keyEvent: React.KeyboardEvent) => {
|
||||||
if (keyEvent.key === 'Enter' && (keyEvent.shiftKey || keyEvent.ctrlKey)) {
|
if (keyEvent.key === 'Enter' && (keyEvent.shiftKey || keyEvent.ctrlKey)) {
|
||||||
@ -255,6 +258,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
|
|||||||
configure it in the <a href={`/datasources/edit/${datasource.uid}`}>datasource settings</a>.
|
configure it in the <a href={`/datasources/edit/${datasource.uid}`}>datasource settings</a>.
|
||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
{alertText && <TemporaryAlert severity="error" text={alertText} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
|
||||||
import { CodeEditor, Monaco, monacoTypes, useTheme2 } from '@grafana/ui';
|
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 { TempoDatasource } from '../../datasource';
|
||||||
|
|
||||||
import { CompletionProvider } from './autocomplete';
|
import { CompletionProvider } from './autocomplete';
|
||||||
@ -21,40 +19,44 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TagsField(props: Props) {
|
export function TagsField(props: Props) {
|
||||||
|
const [alertText, setAlertText] = useState<string>();
|
||||||
const { onChange, onBlur, placeholder } = props;
|
const { onChange, onBlur, placeholder } = props;
|
||||||
const setupAutocompleteFn = useAutocomplete(props.datasource);
|
const setupAutocompleteFn = useAutocomplete(props.datasource, setAlertText);
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme, placeholder);
|
const styles = getStyles(theme, placeholder);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeEditor
|
<>
|
||||||
value={props.value}
|
<CodeEditor
|
||||||
language={langId}
|
value={props.value}
|
||||||
onBlur={onBlur}
|
language={langId}
|
||||||
onChange={onChange}
|
onBlur={onBlur}
|
||||||
containerStyles={styles.queryField}
|
onChange={onChange}
|
||||||
monacoOptions={{
|
containerStyles={styles.queryField}
|
||||||
folding: false,
|
monacoOptions={{
|
||||||
fontSize: 14,
|
folding: false,
|
||||||
lineNumbers: 'off',
|
fontSize: 14,
|
||||||
overviewRulerLanes: 0,
|
lineNumbers: 'off',
|
||||||
renderLineHighlight: 'none',
|
overviewRulerLanes: 0,
|
||||||
scrollbar: {
|
renderLineHighlight: 'none',
|
||||||
vertical: 'hidden',
|
scrollbar: {
|
||||||
verticalScrollbarSize: 8, // used as "padding-right"
|
vertical: 'hidden',
|
||||||
horizontal: 'hidden',
|
verticalScrollbarSize: 8, // used as "padding-right"
|
||||||
horizontalScrollbarSize: 0,
|
horizontal: 'hidden',
|
||||||
},
|
horizontalScrollbarSize: 0,
|
||||||
scrollBeyondLastLine: false,
|
},
|
||||||
wordWrap: 'on',
|
scrollBeyondLastLine: false,
|
||||||
}}
|
wordWrap: 'on',
|
||||||
onBeforeEditorMount={ensureTraceQL}
|
}}
|
||||||
onEditorDidMount={(editor, monaco) => {
|
onBeforeEditorMount={ensureTraceQL}
|
||||||
setupAutocompleteFn(editor, monaco);
|
onEditorDidMount={(editor, monaco) => {
|
||||||
setupPlaceholder(editor, monaco, styles);
|
setupAutocompleteFn(editor, monaco);
|
||||||
setupAutoSize(editor);
|
setupPlaceholder(editor, monaco, styles);
|
||||||
}}
|
setupAutoSize(editor);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
{alertText && <TemporaryAlert severity="error" text={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
|
* 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
|
// 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
|
// 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
|
// 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 () => {
|
const fetchTags = async () => {
|
||||||
try {
|
try {
|
||||||
await datasource.languageProvider.start();
|
await datasource.languageProvider.start();
|
||||||
|
setAlertText(undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
setAlertText(`Error: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchTags();
|
fetchTags();
|
||||||
}, [datasource]);
|
}, [datasource, setAlertText]);
|
||||||
|
|
||||||
const autocompleteDisposeFun = useRef<(() => void) | null>(null);
|
const autocompleteDisposeFun = useRef<(() => void) | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { LanguageProvider } from '@grafana/data';
|
import { LanguageProvider } from '@grafana/data';
|
||||||
import { FetchError } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
||||||
import { TempoDatasource } from '../datasource';
|
import { TempoDatasource } from '../datasource';
|
||||||
@ -290,9 +289,7 @@ const renderSearchField = (
|
|||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
updateFilter={updateFilter}
|
updateFilter={updateFilter}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setError={function (error: FetchError): void {
|
setError={() => {}}
|
||||||
throw error;
|
|
||||||
}}
|
|
||||||
tags={tags || []}
|
tags={tags || []}
|
||||||
hideTag={hideTag}
|
hideTag={hideTag}
|
||||||
query={'{}'}
|
query={'{}'}
|
||||||
|
@ -4,12 +4,10 @@ import React, { useState, useEffect, useMemo } from 'react';
|
|||||||
import useAsync from 'react-use/lib/useAsync';
|
import useAsync from 'react-use/lib/useAsync';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
|
||||||
import { FetchError, getTemplateSrv, isFetchError } from '@grafana/runtime';
|
import { FetchError, getTemplateSrv, isFetchError } from '@grafana/runtime';
|
||||||
import { Select, HorizontalGroup, useStyles2 } from '@grafana/ui';
|
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 { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
||||||
import { TempoDatasource } from '../datasource';
|
import { TempoDatasource } from '../datasource';
|
||||||
import { operators as allOperators, stringOperators, numberOperators, keywordOperators } from '../traceql/traceql';
|
import { operators as allOperators, stringOperators, numberOperators, keywordOperators } from '../traceql/traceql';
|
||||||
@ -26,7 +24,8 @@ interface Props {
|
|||||||
filter: TraceqlFilter;
|
filter: TraceqlFilter;
|
||||||
datasource: TempoDatasource;
|
datasource: TempoDatasource;
|
||||||
updateFilter: (f: TraceqlFilter) => void;
|
updateFilter: (f: TraceqlFilter) => void;
|
||||||
setError: (error: FetchError) => void;
|
deleteFilter?: (f: TraceqlFilter) => void;
|
||||||
|
setError: (error: FetchError | null) => void;
|
||||||
isTagsLoading?: boolean;
|
isTagsLoading?: boolean;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
hideScope?: boolean;
|
hideScope?: boolean;
|
||||||
@ -51,6 +50,7 @@ const SearchField = ({
|
|||||||
allowCustomValue = true,
|
allowCustomValue = true,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const [alertText, setAlertText] = useState<string>();
|
||||||
const scopedTag = useMemo(() => filterScopedTag(filter), [filter]);
|
const scopedTag = useMemo(() => filterScopedTag(filter), [filter]);
|
||||||
// We automatically change the operator to the regex op when users select 2 or more values
|
// 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
|
// However, they expect this to be automatically rolled back to the previous operator once
|
||||||
@ -60,13 +60,16 @@ const SearchField = ({
|
|||||||
|
|
||||||
const updateOptions = async () => {
|
const updateOptions = async () => {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
// Display message if Tempo is connected but search 404's
|
// Display message if Tempo is connected but search 404's
|
||||||
if (isFetchError(error) && error?.status === 404) {
|
if (isFetchError(error) && error?.status === 404) {
|
||||||
setError(error);
|
setError(error);
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
setAlertText(`Error: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@ -135,78 +138,85 @@ const SearchField = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup spacing={'none'} width={'auto'}>
|
<>
|
||||||
{!hideScope && (
|
<HorizontalGroup spacing={'none'} width={'auto'}>
|
||||||
|
{!hideScope && (
|
||||||
|
<Select
|
||||||
|
className={styles.dropdown}
|
||||||
|
inputId={`${filter.id}-scope`}
|
||||||
|
options={withTemplateVariableOptions(scopeOptions)}
|
||||||
|
value={filter.scope}
|
||||||
|
onChange={(v) => {
|
||||||
|
updateFilter({ ...filter, scope: v?.value });
|
||||||
|
}}
|
||||||
|
placeholder="Select scope"
|
||||||
|
aria-label={`select ${filter.id} scope`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!hideTag && (
|
||||||
|
<Select
|
||||||
|
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={withTemplateVariableOptions(
|
||||||
|
(filter.tag !== undefined ? uniq([filter.tag, ...tags]) : tags).map((t) => ({
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Select
|
<Select
|
||||||
className={styles.dropdown}
|
className={styles.dropdown}
|
||||||
inputId={`${filter.id}-scope`}
|
inputId={`${filter.id}-operator`}
|
||||||
options={withTemplateVariableOptions(scopeOptions)}
|
options={withTemplateVariableOptions(operatorList.map(operatorSelectableValue))}
|
||||||
value={filter.scope}
|
value={filter.operator}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
updateFilter({ ...filter, scope: v?.value });
|
updateFilter({ ...filter, operator: v?.value });
|
||||||
}}
|
}}
|
||||||
placeholder="Select scope"
|
isClearable={false}
|
||||||
aria-label={`select ${filter.id} scope`}
|
aria-label={`select ${filter.id} operator`}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!hideTag && (
|
|
||||||
<Select
|
|
||||||
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={withTemplateVariableOptions(
|
|
||||||
(filter.tag !== undefined ? uniq([filter.tag, ...tags]) : tags).map((t) => ({
|
|
||||||
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}
|
allowCustomValue={true}
|
||||||
|
width={8}
|
||||||
/>
|
/>
|
||||||
)}
|
{!hideValue && (
|
||||||
<Select
|
<Select
|
||||||
className={styles.dropdown}
|
className={styles.dropdown}
|
||||||
inputId={`${filter.id}-operator`}
|
inputId={`${filter.id}-value`}
|
||||||
options={withTemplateVariableOptions(operatorList.map(operatorSelectableValue))}
|
isLoading={isLoadingValues}
|
||||||
value={filter.operator}
|
options={withTemplateVariableOptions(options)}
|
||||||
onChange={(v) => {
|
value={filter.value}
|
||||||
updateFilter({ ...filter, operator: v?.value });
|
onChange={(val) => {
|
||||||
}}
|
if (Array.isArray(val)) {
|
||||||
isClearable={false}
|
updateFilter({
|
||||||
aria-label={`select ${filter.id} operator`}
|
...filter,
|
||||||
allowCustomValue={true}
|
value: val.map((v) => v.value),
|
||||||
width={8}
|
valueType: val[0]?.type || uniqueOptionType,
|
||||||
/>
|
});
|
||||||
{!hideValue && (
|
} else {
|
||||||
<Select
|
updateFilter({ ...filter, value: val?.value, valueType: val?.type || uniqueOptionType });
|
||||||
className={styles.dropdown}
|
}
|
||||||
inputId={`${filter.id}-value`}
|
}}
|
||||||
isLoading={isLoadingValues}
|
placeholder="Select value"
|
||||||
options={withTemplateVariableOptions(options)}
|
isClearable={true}
|
||||||
value={filter.value}
|
aria-label={`select ${filter.id} value`}
|
||||||
onChange={(val) => {
|
allowCustomValue={allowCustomValue}
|
||||||
if (Array.isArray(val)) {
|
isMulti={isMulti}
|
||||||
updateFilter({ ...filter, value: val.map((v) => v.value), valueType: val[0]?.type || uniqueOptionType });
|
allowCreateWhileLoading
|
||||||
} else {
|
/>
|
||||||
updateFilter({ ...filter, value: val?.value, valueType: val?.type || uniqueOptionType });
|
)}
|
||||||
}
|
</HorizontalGroup>
|
||||||
}}
|
{alertText && <TemporaryAlert severity="error" text={alertText} />}
|
||||||
placeholder="Select value"
|
</>
|
||||||
isClearable={true}
|
|
||||||
aria-label={`select ${filter.id} value`}
|
|
||||||
allowCustomValue={allowCustomValue}
|
|
||||||
isMulti={isMulti}
|
|
||||||
allowCreateWhileLoading
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</HorizontalGroup>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { FetchError } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
||||||
import { TempoDatasource } from '../datasource';
|
import { TempoDatasource } from '../datasource';
|
||||||
import TempoLanguageProvider from '../language_provider';
|
import TempoLanguageProvider from '../language_provider';
|
||||||
@ -124,9 +122,7 @@ describe('TagsInput', () => {
|
|||||||
updateFilter={jest.fn}
|
updateFilter={jest.fn}
|
||||||
deleteFilter={jest.fn}
|
deleteFilter={jest.fn}
|
||||||
filters={[filter]}
|
filters={[filter]}
|
||||||
setError={function (error: FetchError): void {
|
setError={() => {}}
|
||||||
throw error;
|
|
||||||
}}
|
|
||||||
staticTags={[]}
|
staticTags={[]}
|
||||||
isTagsLoading={false}
|
isTagsLoading={false}
|
||||||
query={''}
|
query={''}
|
||||||
|
@ -34,7 +34,7 @@ interface Props {
|
|||||||
deleteFilter: (f: TraceqlFilter) => void;
|
deleteFilter: (f: TraceqlFilter) => void;
|
||||||
filters: TraceqlFilter[];
|
filters: TraceqlFilter[];
|
||||||
datasource: TempoDatasource;
|
datasource: TempoDatasource;
|
||||||
setError: (error: FetchError) => void;
|
setError: (error: FetchError | null) => void;
|
||||||
staticTags: Array<string | undefined>;
|
staticTags: Array<string | undefined>;
|
||||||
isTagsLoading: boolean;
|
isTagsLoading: boolean;
|
||||||
hideValues?: boolean;
|
hideValues?: boolean;
|
||||||
|
@ -137,8 +137,8 @@ describe('TraceQLSearch', () => {
|
|||||||
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(1); // filled in the default tag, so can remove values
|
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(1); // filled in the default tag, so can remove values
|
||||||
|
|
||||||
await user.click(screen.getAllByLabelText(/Remove tag/)[0]);
|
await user.click(screen.getAllByLabelText(/Remove tag/)[0]);
|
||||||
jest.advanceTimersByTime(1000);
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
|
||||||
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(0); // mot filled in the default tag, so no values to remove
|
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(0); // mot filled in the default tag, so no values to remove
|
||||||
});
|
});
|
||||||
|
@ -2,13 +2,11 @@ import { css } from '@emotion/css';
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { CoreApp, GrafanaTheme2 } from '@grafana/data';
|
import { CoreApp, GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
|
||||||
import { config, FetchError, getTemplateSrv, reportInteraction } from '@grafana/runtime';
|
import { config, FetchError, getTemplateSrv, reportInteraction } from '@grafana/runtime';
|
||||||
import { Alert, Button, HorizontalGroup, Select, useStyles2 } from '@grafana/ui';
|
import { Alert, Button, HorizontalGroup, Select, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { notifyApp } from '../_importedDependencies/actions/appNotification';
|
|
||||||
import { createErrorNotification } from '../_importedDependencies/core/appNotification';
|
|
||||||
import { RawQuery } from '../_importedDependencies/datasources/prometheus/RawQuery';
|
import { RawQuery } from '../_importedDependencies/datasources/prometheus/RawQuery';
|
||||||
import { dispatch } from '../_importedDependencies/store';
|
|
||||||
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
||||||
import { TempoDatasource } from '../datasource';
|
import { TempoDatasource } from '../datasource';
|
||||||
import { TempoQueryBuilderOptions } from '../traceql/TempoQueryBuilderOptions';
|
import { TempoQueryBuilderOptions } from '../traceql/TempoQueryBuilderOptions';
|
||||||
@ -35,6 +33,7 @@ const hardCodedFilterIds = ['min-duration', 'max-duration', 'status'];
|
|||||||
|
|
||||||
const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app }: Props) => {
|
const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const [alertText, setAlertText] = useState<string>();
|
||||||
const [error, setError] = useState<Error | FetchError | null>(null);
|
const [error, setError] = useState<Error | FetchError | null>(null);
|
||||||
|
|
||||||
const [isTagsLoading, setIsTagsLoading] = useState(true);
|
const [isTagsLoading, setIsTagsLoading] = useState(true);
|
||||||
@ -73,14 +72,15 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app }: Pro
|
|||||||
try {
|
try {
|
||||||
await datasource.languageProvider.start();
|
await datasource.languageProvider.start();
|
||||||
setIsTagsLoading(false);
|
setIsTagsLoading(false);
|
||||||
|
setAlertText(undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
setAlertText(`Error: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchTags();
|
fetchTags();
|
||||||
}, [datasource]);
|
}, [datasource, setAlertText]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialize state with configured static filters that already have a value from the config
|
// Initialize state with configured static filters that already have a value from the config
|
||||||
@ -250,6 +250,7 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app }: Pro
|
|||||||
configure it in the <a href={`/datasources/edit/${datasource.uid}`}>datasource settings</a>.
|
configure it in the <a href={`/datasources/edit/${datasource.uid}`}>datasource settings</a>.
|
||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
{alertText && <TemporaryAlert severity={'error'} text={alertText} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,8 +6,8 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { Alert, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui';
|
import { Alert, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { AdHocFilter } from './_importedDependencies/components/AdHocFilter/AdHocFilter';
|
import { AdHocFilter } from './_importedDependencies/components/AdHocFilter/AdHocFilter';
|
||||||
|
import { AdHocVariableFilter } from './_importedDependencies/components/AdHocFilter/types';
|
||||||
import { PrometheusDatasource } from './_importedDependencies/datasources/prometheus/types';
|
import { PrometheusDatasource } from './_importedDependencies/datasources/prometheus/types';
|
||||||
import { AdHocVariableFilter } from './_importedDependencies/types';
|
|
||||||
import { TempoQuery } from './types';
|
import { TempoQuery } from './types';
|
||||||
import { getDS } from './utils';
|
import { getDS } from './utils';
|
||||||
|
|
||||||
|
@ -1,128 +0,0 @@
|
|||||||
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
import { AppNotification, AppNotificationSeverity, AppNotificationsState } from './types/appNotifications';
|
|
||||||
|
|
||||||
const MAX_STORED_NOTIFICATIONS = 25;
|
|
||||||
export const STORAGE_KEY = 'notifications';
|
|
||||||
export const NEW_NOTIFS_KEY = `${STORAGE_KEY}/lastRead`;
|
|
||||||
type StoredNotification = Omit<AppNotification, 'component'>;
|
|
||||||
|
|
||||||
export const initialState: AppNotificationsState = {
|
|
||||||
byId: deserializeNotifications(),
|
|
||||||
lastRead: Number.parseInt(window.localStorage.getItem(NEW_NOTIFS_KEY) ?? `${Date.now()}`, 10),
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reducer and action to show toast notifications of various types (success, warnings, errors etc). Use to show
|
|
||||||
* transient info to user, like errors that cannot be otherwise handled or success after an action.
|
|
||||||
*
|
|
||||||
* Use factory functions in core/copy/appNotifications to create the payload.
|
|
||||||
*/
|
|
||||||
const appNotificationsSlice = createSlice({
|
|
||||||
name: 'appNotifications',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
notifyApp: (state, { payload: newAlert }: PayloadAction<AppNotification>) => {
|
|
||||||
if (Object.values(state.byId).some((alert) => isSimilar(newAlert, alert) && alert.showing)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.byId[newAlert.id] = newAlert;
|
|
||||||
serializeNotifications(state.byId);
|
|
||||||
},
|
|
||||||
hideAppNotification: (state, { payload: alertId }: PayloadAction<string>) => {
|
|
||||||
if (!(alertId in state.byId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.byId[alertId].showing = false;
|
|
||||||
serializeNotifications(state.byId);
|
|
||||||
},
|
|
||||||
clearNotification: (state, { payload: alertId }: PayloadAction<string>) => {
|
|
||||||
delete state.byId[alertId];
|
|
||||||
serializeNotifications(state.byId);
|
|
||||||
},
|
|
||||||
clearAllNotifications: (state) => {
|
|
||||||
state.byId = {};
|
|
||||||
serializeNotifications(state.byId);
|
|
||||||
},
|
|
||||||
readAllNotifications: (state, { payload: timestamp }: PayloadAction<number>) => {
|
|
||||||
state.lastRead = timestamp;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { notifyApp, hideAppNotification, clearNotification, clearAllNotifications, readAllNotifications } =
|
|
||||||
appNotificationsSlice.actions;
|
|
||||||
|
|
||||||
export const appNotificationsReducer = appNotificationsSlice.reducer;
|
|
||||||
|
|
||||||
// Selectors
|
|
||||||
|
|
||||||
export const selectLastReadTimestamp = (state: AppNotificationsState) => state.lastRead;
|
|
||||||
export const selectById = (state: AppNotificationsState) => state.byId;
|
|
||||||
export const selectAll = createSelector(selectById, (byId) =>
|
|
||||||
Object.values(byId).sort((a, b) => b.timestamp - a.timestamp)
|
|
||||||
);
|
|
||||||
export const selectWarningsAndErrors = createSelector(selectAll, (all) => all.filter(isAtLeastWarning));
|
|
||||||
export const selectVisible = createSelector(selectById, (byId) => Object.values(byId).filter((n) => n.showing));
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
|
|
||||||
function isSimilar(a: AppNotification, b: AppNotification): boolean {
|
|
||||||
return a.icon === b.icon && a.severity === b.severity && a.text === b.text && a.title === b.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAtLeastWarning(notif: AppNotification) {
|
|
||||||
return notif.severity === AppNotificationSeverity.Warning || notif.severity === AppNotificationSeverity.Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStoredNotification(obj: unknown): obj is StoredNotification {
|
|
||||||
return typeof obj === 'object' && obj !== null && 'id' in obj && 'icon' in obj && 'title' in obj && 'text' in obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
// (De)serialization
|
|
||||||
|
|
||||||
export function deserializeNotifications(): Record<string, StoredNotification> {
|
|
||||||
const storedNotifsRaw = window.localStorage.getItem(STORAGE_KEY);
|
|
||||||
if (!storedNotifsRaw) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = JSON.parse(storedNotifsRaw);
|
|
||||||
if (!Object.values(parsed).every((v) => isStoredNotification(v))) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeNotifications(notifs: Record<string, StoredNotification>) {
|
|
||||||
const reducedNotifs = Object.values(notifs)
|
|
||||||
.filter(isAtLeastWarning)
|
|
||||||
.sort((a, b) => b.timestamp - a.timestamp)
|
|
||||||
.slice(0, MAX_STORED_NOTIFICATIONS)
|
|
||||||
.reduce<Record<string, StoredNotification>>((prev, cur) => {
|
|
||||||
prev[cur.id] = {
|
|
||||||
id: cur.id,
|
|
||||||
severity: cur.severity,
|
|
||||||
icon: cur.icon,
|
|
||||||
title: cur.title,
|
|
||||||
text: cur.text,
|
|
||||||
traceId: cur.traceId,
|
|
||||||
timestamp: cur.timestamp,
|
|
||||||
// we don't care about still showing toasts after refreshing
|
|
||||||
// https://github.com/grafana/grafana/issues/71932
|
|
||||||
showing: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return prev;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(reducedNotifs));
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Unable to persist notifications to local storage');
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import { appNotificationsReducer as appNotifications } from './appNotification';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
appNotifications,
|
|
||||||
};
|
|
@ -1,36 +0,0 @@
|
|||||||
export interface AppNotification {
|
|
||||||
id: string;
|
|
||||||
severity: AppNotificationSeverity;
|
|
||||||
icon: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
traceId?: string;
|
|
||||||
component?: React.ReactElement;
|
|
||||||
showing: boolean;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AppNotificationSeverity {
|
|
||||||
Success = 'success',
|
|
||||||
Warning = 'warning',
|
|
||||||
Error = 'error',
|
|
||||||
Info = 'info',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AppNotificationTimeout {
|
|
||||||
Success = 3000,
|
|
||||||
Warning = 5000,
|
|
||||||
Error = 7000,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const timeoutMap = {
|
|
||||||
[AppNotificationSeverity.Success]: AppNotificationTimeout.Success,
|
|
||||||
[AppNotificationSeverity.Warning]: AppNotificationTimeout.Warning,
|
|
||||||
[AppNotificationSeverity.Error]: AppNotificationTimeout.Error,
|
|
||||||
[AppNotificationSeverity.Info]: AppNotificationTimeout.Success,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface AppNotificationsState {
|
|
||||||
byId: Record<string, AppNotification>;
|
|
||||||
lastRead: number;
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './appNotifications';
|
|
@ -1,46 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { AppNotification, AppNotificationSeverity } from '../actions/types';
|
|
||||||
|
|
||||||
import { getMessageFromError } from './errors';
|
|
||||||
|
|
||||||
const defaultSuccessNotification = {
|
|
||||||
title: '',
|
|
||||||
text: '',
|
|
||||||
severity: AppNotificationSeverity.Success,
|
|
||||||
icon: 'check',
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultErrorNotification = {
|
|
||||||
title: '',
|
|
||||||
text: '',
|
|
||||||
severity: AppNotificationSeverity.Error,
|
|
||||||
icon: 'exclamation-triangle',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createSuccessNotification = (title: string, text = '', traceId?: string): AppNotification => ({
|
|
||||||
...defaultSuccessNotification,
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
id: uuidv4(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
showing: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createErrorNotification = (
|
|
||||||
title: string,
|
|
||||||
text: string | Error = '',
|
|
||||||
traceId?: string,
|
|
||||||
component?: React.ReactElement
|
|
||||||
): AppNotification => {
|
|
||||||
return {
|
|
||||||
...defaultErrorNotification,
|
|
||||||
text: getMessageFromError(text),
|
|
||||||
title,
|
|
||||||
id: uuidv4(),
|
|
||||||
traceId,
|
|
||||||
component,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
showing: true,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,21 +0,0 @@
|
|||||||
import { isFetchError } from '@grafana/runtime';
|
|
||||||
|
|
||||||
export function getMessageFromError(err: unknown): string {
|
|
||||||
if (typeof err === 'string') {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
return err.message;
|
|
||||||
} else if (isFetchError(err)) {
|
|
||||||
if (err.data && err.data.message) {
|
|
||||||
return err.data.message;
|
|
||||||
} else if (err.statusText) {
|
|
||||||
return err.statusText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(err);
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import { Store } from 'redux';
|
|
||||||
|
|
||||||
export let store: Store<StoreState>;
|
|
||||||
export const initialKeyedVariablesState: any = { keys: {} };
|
|
||||||
|
|
||||||
type StoreState = ReturnType<ReturnType<any>>;
|
|
||||||
|
|
||||||
export function setStore(newStore: Store<StoreState>) {
|
|
||||||
store = newStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getState(): StoreState {
|
|
||||||
if (!store || !store.getState) {
|
|
||||||
return { templating: { ...initialKeyedVariablesState, lastKey: 'key' } } as StoreState; // used by tests
|
|
||||||
}
|
|
||||||
|
|
||||||
return store.getState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This was `any` before
|
|
||||||
export function dispatch(action: any) {
|
|
||||||
if (!store || !store.getState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return store.dispatch(action);
|
|
||||||
}
|
|
@ -20,7 +20,6 @@
|
|||||||
"@opentelemetry/api": "1.7.0",
|
"@opentelemetry/api": "1.7.0",
|
||||||
"@opentelemetry/exporter-collector": "0.25.0",
|
"@opentelemetry/exporter-collector": "0.25.0",
|
||||||
"@opentelemetry/semantic-conventions": "1.21.0",
|
"@opentelemetry/semantic-conventions": "1.21.0",
|
||||||
"@reduxjs/toolkit": "1.9.5",
|
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
"events": "3.3.0",
|
"events": "3.3.0",
|
||||||
"i18next": "^23.0.0",
|
"i18next": "^23.0.0",
|
||||||
@ -31,7 +30,6 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-use": "17.5.0",
|
"react-use": "17.5.0",
|
||||||
"redux": "4.2.1",
|
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"semver": "7.6.0",
|
"semver": "7.6.0",
|
||||||
"stream-browserify": "3.0.0",
|
"stream-browserify": "3.0.0",
|
||||||
|
@ -23,7 +23,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TraceQLEditor(props: Props) {
|
export function TraceQLEditor(props: Props) {
|
||||||
const [alertText, setAlertText] = useState('');
|
const [alertText, setAlertText] = useState<string>();
|
||||||
|
|
||||||
const { query, onChange, onRunQuery, placeholder } = props;
|
const { query, onChange, onRunQuery, placeholder } = props;
|
||||||
const setupAutocompleteFn = useAutocomplete(props.datasource, setAlertText);
|
const setupAutocompleteFn = useAutocomplete(props.datasource, setAlertText);
|
||||||
@ -116,7 +116,7 @@ export function TraceQLEditor(props: Props) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{alertText && <TemporaryAlert severity={'error'} text={alertText} />}
|
{alertText && <TemporaryAlert severity="error" text={alertText} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -193,23 +193,23 @@ function setupAutoSize(editor: monacoTypes.editor.IStandaloneCodeEditor) {
|
|||||||
* @param datasource the Tempo datasource instance
|
* @param datasource the Tempo datasource instance
|
||||||
* @param setAlertText setter for alert's text
|
* @param setAlertText setter for alert's text
|
||||||
*/
|
*/
|
||||||
function useAutocomplete(datasource: TempoDatasource, setAlertText: (text: string) => void) {
|
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
|
// 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
|
// 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
|
// returned function but that is run after the monaco is mounted so would delay the request a bit when it does not
|
||||||
// need to.
|
// need to.
|
||||||
const providerRef = useRef<CompletionProvider>(
|
const providerRef = useRef<CompletionProvider>(
|
||||||
new CompletionProvider({ languageProvider: datasource.languageProvider })
|
new CompletionProvider({ languageProvider: datasource.languageProvider, setAlertText })
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTags = async () => {
|
const fetchTags = async () => {
|
||||||
try {
|
try {
|
||||||
await datasource.languageProvider.start();
|
await datasource.languageProvider.start();
|
||||||
|
setAlertText(undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error(error);
|
setAlertText(`Error: ${error.message}`);
|
||||||
setAlertText(error.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -395,7 +395,7 @@ function setup(value: string, offset: number, tagsV1?: string[], tagsV2?: Scope[
|
|||||||
} else if (tagsV2) {
|
} else if (tagsV2) {
|
||||||
lp.setV2Tags(tagsV2);
|
lp.setV2Tags(tagsV2);
|
||||||
}
|
}
|
||||||
const provider = new CompletionProvider({ languageProvider: lp });
|
const provider = new CompletionProvider({ languageProvider: lp, setAlertText: () => {} });
|
||||||
const model = makeModel(value, offset);
|
const model = makeModel(value, offset);
|
||||||
provider.monaco = {
|
provider.monaco = {
|
||||||
Range: {
|
Range: {
|
||||||
|
@ -4,9 +4,6 @@ import { SelectableValue } from '@grafana/data';
|
|||||||
import { isFetchError } from '@grafana/runtime';
|
import { isFetchError } from '@grafana/runtime';
|
||||||
import type { Monaco, monacoTypes } from '@grafana/ui';
|
import type { Monaco, monacoTypes } from '@grafana/ui';
|
||||||
|
|
||||||
import { notifyApp } from '../_importedDependencies/actions/appNotification';
|
|
||||||
import { createErrorNotification } from '../_importedDependencies/core/appNotification';
|
|
||||||
import { dispatch } from '../_importedDependencies/store';
|
|
||||||
import TempoLanguageProvider from '../language_provider';
|
import TempoLanguageProvider from '../language_provider';
|
||||||
|
|
||||||
import { getSituation, Situation } from './situation';
|
import { getSituation, Situation } from './situation';
|
||||||
@ -14,6 +11,7 @@ import { intrinsics, scopes } from './traceql';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
languageProvider: TempoLanguageProvider;
|
languageProvider: TempoLanguageProvider;
|
||||||
|
setAlertText: (text?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MinimalCompletionItem = {
|
type MinimalCompletionItem = {
|
||||||
@ -33,9 +31,11 @@ type MinimalCompletionItem = {
|
|||||||
export class CompletionProvider implements monacoTypes.languages.CompletionItemProvider {
|
export class CompletionProvider implements monacoTypes.languages.CompletionItemProvider {
|
||||||
languageProvider: TempoLanguageProvider;
|
languageProvider: TempoLanguageProvider;
|
||||||
registerInteractionCommandId: string | null;
|
registerInteractionCommandId: string | null;
|
||||||
|
setAlertText: (text?: string) => void;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
this.languageProvider = props.languageProvider;
|
this.languageProvider = props.languageProvider;
|
||||||
|
this.setAlertText = props.setAlertText;
|
||||||
this.registerInteractionCommandId = null;
|
this.registerInteractionCommandId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,7 +243,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
|
|||||||
|
|
||||||
const { range, offset } = getRangeAndOffset(this.monaco, model, position);
|
const { range, offset } = getRangeAndOffset(this.monaco, model, position);
|
||||||
const situation = getSituation(model.getValue(), offset);
|
const situation = getSituation(model.getValue(), offset);
|
||||||
const completionItems = situation != null ? this.getCompletions(situation) : Promise.resolve([]);
|
const completionItems = situation != null ? this.getCompletions(situation, this.setAlertText) : Promise.resolve([]);
|
||||||
|
|
||||||
return completionItems.then((items) => {
|
return completionItems.then((items) => {
|
||||||
// monaco by-default alphabetically orders the items.
|
// monaco by-default alphabetically orders the items.
|
||||||
@ -298,7 +298,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
|
|||||||
* @param situation
|
* @param situation
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async getCompletions(situation: Situation): Promise<Completion[]> {
|
private async getCompletions(situation: Situation, setAlertText: (text?: string) => void): Promise<Completion[]> {
|
||||||
switch (situation.type) {
|
switch (situation.type) {
|
||||||
// This should only happen for cases that we do not support yet
|
// This should only happen for cases that we do not support yet
|
||||||
case 'UNKNOWN': {
|
case 'UNKNOWN': {
|
||||||
@ -370,11 +370,12 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
|
|||||||
let tagValues;
|
let tagValues;
|
||||||
try {
|
try {
|
||||||
tagValues = await this.getTagValues(situation.tagName, situation.query);
|
tagValues = await this.getTagValues(situation.tagName, situation.query);
|
||||||
|
setAlertText(undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isFetchError(error)) {
|
if (isFetchError(error)) {
|
||||||
dispatch(notifyApp(createErrorNotification(error.data.error, new Error(error.data.message))));
|
setAlertText(error.data.error);
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
setAlertText(`Error: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3445,7 +3445,6 @@ __metadata:
|
|||||||
"@opentelemetry/api": "npm:1.7.0"
|
"@opentelemetry/api": "npm:1.7.0"
|
||||||
"@opentelemetry/exporter-collector": "npm:0.25.0"
|
"@opentelemetry/exporter-collector": "npm:0.25.0"
|
||||||
"@opentelemetry/semantic-conventions": "npm:1.21.0"
|
"@opentelemetry/semantic-conventions": "npm:1.21.0"
|
||||||
"@reduxjs/toolkit": "npm:1.9.5"
|
|
||||||
"@testing-library/jest-dom": "npm:6.4.2"
|
"@testing-library/jest-dom": "npm:6.4.2"
|
||||||
"@testing-library/react": "npm:14.2.1"
|
"@testing-library/react": "npm:14.2.1"
|
||||||
"@testing-library/user-event": "npm:14.5.2"
|
"@testing-library/user-event": "npm:14.5.2"
|
||||||
@ -3469,7 +3468,6 @@ __metadata:
|
|||||||
react-dom: "npm:18.2.0"
|
react-dom: "npm:18.2.0"
|
||||||
react-select-event: "npm:5.5.1"
|
react-select-event: "npm:5.5.1"
|
||||||
react-use: "npm:17.5.0"
|
react-use: "npm:17.5.0"
|
||||||
redux: "npm:4.2.1"
|
|
||||||
rxjs: "npm:7.8.1"
|
rxjs: "npm:7.8.1"
|
||||||
semver: "npm:7.6.0"
|
semver: "npm:7.6.0"
|
||||||
stream-browserify: "npm:3.0.0"
|
stream-browserify: "npm:3.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user