mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tempo: Add filtering for service graph query (#41162)
* Add filter based on AdHocFilter element * Add tests * Cancel layout in case we have have new data or we unmount node graph * Fix typing * Fix test
This commit is contained in:
280
public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx
Normal file
280
public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
InlineFieldRow,
|
||||
InlineField,
|
||||
Input,
|
||||
QueryField,
|
||||
SlatePrism,
|
||||
BracesPlugin,
|
||||
TypeaheadInput,
|
||||
TypeaheadOutput,
|
||||
Select,
|
||||
Alert,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { tokenizer } from '../syntax';
|
||||
import Prism from 'prismjs';
|
||||
import { Node } from 'slate';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2, isValidGoDuration, SelectableValue } from '@grafana/data';
|
||||
import TempoLanguageProvider from '../language_provider';
|
||||
import { TempoDatasource, TempoQuery } from '../datasource';
|
||||
import { debounce } from 'lodash';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
|
||||
interface Props {
|
||||
datasource: TempoDatasource;
|
||||
query: TempoQuery;
|
||||
onChange: (value: TempoQuery) => void;
|
||||
onBlur?: () => void;
|
||||
onRunQuery: () => void;
|
||||
}
|
||||
|
||||
const PRISM_LANGUAGE = 'tempo';
|
||||
const durationPlaceholder = 'e.g. 1.2s, 100ms';
|
||||
const plugins = [
|
||||
BracesPlugin(),
|
||||
SlatePrism({
|
||||
onlyIn: (node: Node) => node.object === 'block' && node.type === 'code_block',
|
||||
getSyntax: () => PRISM_LANGUAGE,
|
||||
}),
|
||||
];
|
||||
|
||||
Prism.languages[PRISM_LANGUAGE] = tokenizer;
|
||||
|
||||
const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]);
|
||||
const [hasSyntaxLoaded, setHasSyntaxLoaded] = useState(false);
|
||||
const [autocomplete, setAutocomplete] = useState<{
|
||||
serviceNameOptions: Array<SelectableValue<string>>;
|
||||
spanNameOptions: Array<SelectableValue<string>>;
|
||||
}>({
|
||||
serviceNameOptions: [],
|
||||
spanNameOptions: [],
|
||||
});
|
||||
const [error, setError] = useState(null);
|
||||
const [inputErrors, setInputErrors] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
const fetchServiceNameOptions = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
async () => {
|
||||
const res = await languageProvider.getOptions('service.name');
|
||||
setAutocomplete((prev) => ({ ...prev, serviceNameOptions: res }));
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: true }
|
||||
),
|
||||
[languageProvider]
|
||||
);
|
||||
|
||||
const fetchSpanNameOptions = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
async () => {
|
||||
const res = await languageProvider.getOptions('name');
|
||||
setAutocomplete((prev) => ({ ...prev, spanNameOptions: res }));
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: true }
|
||||
),
|
||||
[languageProvider]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAutocomplete = async () => {
|
||||
try {
|
||||
await languageProvider.start();
|
||||
const serviceNameOptions = await languageProvider.getOptions('service.name');
|
||||
const spanNameOptions = await languageProvider.getOptions('name');
|
||||
setHasSyntaxLoaded(true);
|
||||
setAutocomplete({ serviceNameOptions, spanNameOptions });
|
||||
} catch (error) {
|
||||
// Display message if Tempo is connected but search 404's
|
||||
if (error?.status === 404) {
|
||||
setError(error);
|
||||
} else {
|
||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchAutocomplete();
|
||||
}, [languageProvider, fetchServiceNameOptions, fetchSpanNameOptions]);
|
||||
|
||||
const onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
|
||||
return await languageProvider.provideCompletionItems(typeahead);
|
||||
};
|
||||
|
||||
const cleanText = (text: string) => {
|
||||
const splittedText = text.split(/\s+(?=([^"]*"[^"]*")*[^"]*$)/g);
|
||||
if (splittedText.length > 1) {
|
||||
return splittedText[splittedText.length - 1];
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const onKeyDown = (keyEvent: React.KeyboardEvent) => {
|
||||
if (keyEvent.key === 'Enter' && (keyEvent.shiftKey || keyEvent.ctrlKey)) {
|
||||
onRunQuery();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Service Name" labelWidth={14} grow>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
options={autocomplete.serviceNameOptions}
|
||||
value={query.serviceName || ''}
|
||||
onChange={(v) => {
|
||||
onChange({
|
||||
...query,
|
||||
serviceName: v?.value || undefined,
|
||||
});
|
||||
}}
|
||||
placeholder="Select a service"
|
||||
onOpenMenu={fetchServiceNameOptions}
|
||||
isClearable
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Span Name" labelWidth={14} grow>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
options={autocomplete.spanNameOptions}
|
||||
value={query.spanName || ''}
|
||||
onChange={(v) => {
|
||||
onChange({
|
||||
...query,
|
||||
spanName: v?.value || undefined,
|
||||
});
|
||||
}}
|
||||
placeholder="Select a span"
|
||||
onOpenMenu={fetchSpanNameOptions}
|
||||
isClearable
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Tags" labelWidth={14} grow tooltip="Values should be in the logfmt format.">
|
||||
<QueryField
|
||||
additionalPlugins={plugins}
|
||||
query={query.search}
|
||||
onTypeahead={onTypeahead}
|
||||
onBlur={onBlur}
|
||||
onChange={(value) => {
|
||||
onChange({
|
||||
...query,
|
||||
search: value,
|
||||
});
|
||||
}}
|
||||
placeholder="http.status_code=200 error=true"
|
||||
cleanText={cleanText}
|
||||
onRunQuery={onRunQuery}
|
||||
syntaxLoaded={hasSyntaxLoaded}
|
||||
portalOrigin="tempo"
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Min Duration" invalid={!!inputErrors.minDuration} labelWidth={14} grow>
|
||||
<Input
|
||||
value={query.minDuration || ''}
|
||||
placeholder={durationPlaceholder}
|
||||
onBlur={() => {
|
||||
if (query.minDuration && !isValidGoDuration(query.minDuration)) {
|
||||
setInputErrors({ ...inputErrors, minDuration: true });
|
||||
} else {
|
||||
setInputErrors({ ...inputErrors, minDuration: false });
|
||||
}
|
||||
}}
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
minDuration: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Max Duration" invalid={!!inputErrors.maxDuration} labelWidth={14} grow>
|
||||
<Input
|
||||
value={query.maxDuration || ''}
|
||||
placeholder={durationPlaceholder}
|
||||
onBlur={() => {
|
||||
if (query.maxDuration && !isValidGoDuration(query.maxDuration)) {
|
||||
setInputErrors({ ...inputErrors, maxDuration: true });
|
||||
} else {
|
||||
setInputErrors({ ...inputErrors, maxDuration: false });
|
||||
}
|
||||
}}
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
maxDuration: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Limit"
|
||||
invalid={!!inputErrors.limit}
|
||||
labelWidth={14}
|
||||
grow
|
||||
tooltip="Maximum numbers of returned results"
|
||||
>
|
||||
<Input
|
||||
value={query.limit || ''}
|
||||
type="number"
|
||||
onChange={(v) => {
|
||||
let limit = v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined;
|
||||
if (limit && (!Number.isInteger(limit) || limit <= 0)) {
|
||||
setInputErrors({ ...inputErrors, limit: true });
|
||||
} else {
|
||||
setInputErrors({ ...inputErrors, limit: false });
|
||||
}
|
||||
|
||||
onChange({
|
||||
...query,
|
||||
limit: v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined,
|
||||
});
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
{error ? (
|
||||
<Alert title="Unable to connect to Tempo search" severity="info" className={styles.alert}>
|
||||
Please ensure that Tempo is configured with search enabled. If you would like to hide this tab, you can
|
||||
configure it in the <a href={`/datasources/edit/${datasource.uid}`}>datasource settings</a>.
|
||||
</Alert>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NativeSearch;
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css`
|
||||
max-width: 500px;
|
||||
`,
|
||||
alert: css`
|
||||
max-width: 75ch;
|
||||
margin-top: ${theme.spacing(2)};
|
||||
`,
|
||||
});
|
||||
247
public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx
Normal file
247
public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
Badge,
|
||||
FileDropzone,
|
||||
InlineField,
|
||||
InlineFieldRow,
|
||||
InlineLabel,
|
||||
QueryField,
|
||||
RadioButtonGroup,
|
||||
Themeable2,
|
||||
withTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings';
|
||||
import React from 'react';
|
||||
import { LokiQueryField } from '../../loki/components/LokiQueryField';
|
||||
import { LokiQuery } from '../../loki/types';
|
||||
import { TempoDatasource, TempoQuery, TempoQueryType } from '../datasource';
|
||||
import LokiDatasource from '../../loki/datasource';
|
||||
import { PrometheusDatasource } from '../../prometheus/datasource';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import NativeSearch from './NativeSearch';
|
||||
import { getDS } from './utils';
|
||||
import { ServiceGraphSection } from './ServiceGraphSection';
|
||||
|
||||
interface Props extends QueryEditorProps<TempoDatasource, TempoQuery>, Themeable2 {}
|
||||
|
||||
const DEFAULT_QUERY_TYPE: TempoQueryType = 'traceId';
|
||||
|
||||
interface State {
|
||||
linkedDatasourceUid?: string;
|
||||
linkedDatasource?: LokiDatasource;
|
||||
serviceMapDatasourceUid?: string;
|
||||
serviceMapDatasource?: PrometheusDatasource;
|
||||
}
|
||||
|
||||
class TempoQueryFieldComponent extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
linkedDatasourceUid: undefined,
|
||||
linkedDatasource: undefined,
|
||||
serviceMapDatasourceUid: undefined,
|
||||
serviceMapDatasource: undefined,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { datasource } = this.props;
|
||||
// Find query field from linked datasource
|
||||
const tracesToLogsOptions: TraceToLogsOptions = datasource.tracesToLogs || {};
|
||||
const linkedDatasourceUid = tracesToLogsOptions.datasourceUid;
|
||||
|
||||
const serviceMapDsUid = datasource.serviceMap?.datasourceUid;
|
||||
|
||||
// Check status of linked data sources so we can show warnings if needed.
|
||||
const [logsDs, serviceMapDs] = await Promise.all([getDS(linkedDatasourceUid), getDS(serviceMapDsUid)]);
|
||||
|
||||
this.setState({
|
||||
linkedDatasourceUid: linkedDatasourceUid,
|
||||
linkedDatasource: logsDs as LokiDatasource,
|
||||
serviceMapDatasourceUid: serviceMapDsUid,
|
||||
serviceMapDatasource: serviceMapDs as PrometheusDatasource,
|
||||
});
|
||||
|
||||
// Set initial query type to ensure traceID field appears
|
||||
if (!this.props.query.queryType) {
|
||||
this.props.onChange({
|
||||
...this.props.query,
|
||||
queryType: DEFAULT_QUERY_TYPE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChangeLinkedQuery = (value: LokiQuery) => {
|
||||
const { query, onChange } = this.props;
|
||||
onChange({
|
||||
...query,
|
||||
linkedQuery: { ...value, refId: 'linked' },
|
||||
});
|
||||
};
|
||||
|
||||
onRunLinkedQuery = () => {
|
||||
this.props.onRunQuery();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { query, onChange, datasource } = this.props;
|
||||
// Find query field from linked datasource
|
||||
const tracesToLogsOptions: TraceToLogsOptions = datasource.tracesToLogs || {};
|
||||
const logsDatasourceUid = tracesToLogsOptions.datasourceUid;
|
||||
const graphDatasourceUid = datasource.serviceMap?.datasourceUid;
|
||||
|
||||
const queryTypeOptions: Array<SelectableValue<TempoQueryType>> = [
|
||||
{ value: 'traceId', label: 'TraceID' },
|
||||
{ value: 'upload', label: 'JSON file' },
|
||||
];
|
||||
|
||||
if (config.featureToggles.tempoServiceGraph) {
|
||||
queryTypeOptions.push({ value: 'serviceMap', label: 'Service Graph' });
|
||||
}
|
||||
|
||||
if (config.featureToggles.tempoSearch && !datasource?.search?.hide) {
|
||||
queryTypeOptions.unshift({ value: 'nativeSearch', label: 'Search - Beta' });
|
||||
}
|
||||
|
||||
if (logsDatasourceUid && tracesToLogsOptions?.lokiSearch !== false) {
|
||||
if (!config.featureToggles.tempoSearch) {
|
||||
// Place at beginning as Search if no native search
|
||||
queryTypeOptions.unshift({ value: 'search', label: 'Search' });
|
||||
} else {
|
||||
// Place at end as Loki Search if native search is enabled
|
||||
queryTypeOptions.push({ value: 'search', label: 'Loki Search' });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Query type">
|
||||
<RadioButtonGroup<TempoQueryType>
|
||||
options={queryTypeOptions}
|
||||
value={query.queryType}
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
queryType: v,
|
||||
})
|
||||
}
|
||||
size="md"
|
||||
/>
|
||||
</InlineField>
|
||||
|
||||
{query.queryType === 'nativeSearch' && (
|
||||
<p>
|
||||
<Badge icon="rocket" text="Beta" color="blue" />
|
||||
Tempo search is currently in beta and is designed to return recent traces only. It ignores the time
|
||||
range picker. We are actively working on full backend search. Look for improvements in the near future!
|
||||
</p>
|
||||
)}
|
||||
</InlineFieldRow>
|
||||
{query.queryType === 'search' && (
|
||||
<SearchSection
|
||||
linkedDatasourceUid={logsDatasourceUid}
|
||||
query={query}
|
||||
onRunQuery={this.onRunLinkedQuery}
|
||||
onChange={this.onChangeLinkedQuery}
|
||||
/>
|
||||
)}
|
||||
{query.queryType === 'nativeSearch' && (
|
||||
<NativeSearch
|
||||
datasource={this.props.datasource}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onBlur={this.props.onBlur}
|
||||
onRunQuery={this.props.onRunQuery}
|
||||
/>
|
||||
)}
|
||||
{query.queryType === 'upload' && (
|
||||
<div className={css({ padding: this.props.theme.spacing(2) })}>
|
||||
<FileDropzone
|
||||
options={{ multiple: false }}
|
||||
onLoad={(result) => {
|
||||
this.props.datasource.uploadedJson = result;
|
||||
this.props.onRunQuery();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{query.queryType === 'traceId' && (
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Trace ID" labelWidth={14} grow>
|
||||
<QueryField
|
||||
query={query.query}
|
||||
onChange={(val) => {
|
||||
onChange({
|
||||
...query,
|
||||
query: val,
|
||||
queryType: 'traceId',
|
||||
linkedQuery: undefined,
|
||||
});
|
||||
}}
|
||||
onBlur={this.props.onBlur}
|
||||
onRunQuery={this.props.onRunQuery}
|
||||
placeholder={'Enter a Trace ID (run with Shift+Enter)'}
|
||||
portalOrigin="tempo"
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
{query.queryType === 'serviceMap' && (
|
||||
<ServiceGraphSection graphDatasourceUid={graphDatasourceUid} query={query} onChange={onChange} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchSectionProps {
|
||||
linkedDatasourceUid?: string;
|
||||
onChange: (value: LokiQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
query: TempoQuery;
|
||||
}
|
||||
function SearchSection({ linkedDatasourceUid, onChange, onRunQuery, query }: SearchSectionProps) {
|
||||
const dsState = useAsync(() => getDS(linkedDatasourceUid), [linkedDatasourceUid]);
|
||||
if (dsState.loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ds = dsState.value as LokiDatasource;
|
||||
|
||||
if (ds) {
|
||||
return (
|
||||
<>
|
||||
<InlineLabel>Tempo uses {ds.name} to find traces.</InlineLabel>
|
||||
|
||||
<LokiQueryField
|
||||
datasource={ds}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
query={query.linkedQuery ?? ({ refId: 'linked' } as any)}
|
||||
history={[]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!linkedDatasourceUid) {
|
||||
return <div className="text-warning">Please set up a Traces-to-logs datasource in the datasource settings.</div>;
|
||||
}
|
||||
|
||||
if (linkedDatasourceUid && !ds) {
|
||||
return (
|
||||
<div className="text-warning">
|
||||
Traces-to-logs datasource is configured but the data source no longer exists. Please configure existing data
|
||||
source to use the search.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const TempoQueryField = withTheme2(TempoQueryFieldComponent);
|
||||
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { getDS } from './utils';
|
||||
import { InlineField, InlineFieldRow } from '@grafana/ui';
|
||||
import { AdHocVariableFilter } from '../../../../features/variables/types';
|
||||
import { TempoQuery } from '../datasource';
|
||||
import { AdHocFilter } from '../../../../features/variables/adhoc/picker/AdHocFilter';
|
||||
import { PrometheusDatasource } from '../../prometheus/datasource';
|
||||
|
||||
export function ServiceGraphSection({
|
||||
graphDatasourceUid,
|
||||
query,
|
||||
onChange,
|
||||
}: {
|
||||
graphDatasourceUid?: string;
|
||||
query: TempoQuery;
|
||||
onChange: (value: TempoQuery) => void;
|
||||
}) {
|
||||
const dsState = useAsync(() => getDS(graphDatasourceUid), [graphDatasourceUid]);
|
||||
if (dsState.loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ds = dsState.value as PrometheusDatasource;
|
||||
|
||||
if (!graphDatasourceUid) {
|
||||
return <div className="text-warning">Please set up a service graph datasource in the datasource settings.</div>;
|
||||
}
|
||||
|
||||
if (graphDatasourceUid && !ds) {
|
||||
return (
|
||||
<div className="text-warning">
|
||||
Service graph datasource is configured but the data source no longer exists. Please configure existing data
|
||||
source to use the service graph functionality.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const filters = queryToFilter(query.serviceMapQuery || '');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Filter" labelWidth={14} grow>
|
||||
<AdHocFilter
|
||||
datasource={{ uid: graphDatasourceUid }}
|
||||
filters={filters}
|
||||
addFilter={(filter: AdHocVariableFilter) => {
|
||||
onChange({
|
||||
...query,
|
||||
serviceMapQuery: filtersToQuery([...filters, filter]),
|
||||
});
|
||||
}}
|
||||
removeFilter={(index: number) => {
|
||||
const newFilters = [...filters];
|
||||
newFilters.splice(index, 1);
|
||||
onChange({ ...query, serviceMapQuery: filtersToQuery(newFilters) });
|
||||
}}
|
||||
changeFilter={(index: number, filter: AdHocVariableFilter) => {
|
||||
const newFilters = [...filters];
|
||||
newFilters.splice(index, 1, filter);
|
||||
onChange({ ...query, serviceMapQuery: filtersToQuery(newFilters) });
|
||||
}}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function queryToFilter(query: string): AdHocVariableFilter[] {
|
||||
let match;
|
||||
let filters: AdHocVariableFilter[] = [];
|
||||
const re = /([\w_]+)(=|!=|<|>|=~|!~)"(.*?)"/g;
|
||||
while ((match = re.exec(query)) !== null) {
|
||||
filters.push({
|
||||
key: match[1],
|
||||
operator: match[2],
|
||||
value: match[3],
|
||||
condition: '',
|
||||
});
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
function filtersToQuery(filters: AdHocVariableFilter[]): string {
|
||||
return `{${filters.map((f) => `${f.key}${f.operator}"${f.value}"`).join(',')}}`;
|
||||
}
|
||||
16
public/app/plugins/datasource/tempo/QueryEditor/utils.ts
Normal file
16
public/app/plugins/datasource/tempo/QueryEditor/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { DataSourceApi } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
|
||||
export async function getDS(uid?: string): Promise<DataSourceApi | undefined> {
|
||||
if (!uid) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dsSrv = getDataSourceSrv();
|
||||
try {
|
||||
return await dsSrv.get(uid);
|
||||
} catch (error) {
|
||||
console.error('Failed to load data source', error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user