mirror of https://github.com/grafana/grafana
Tempo: TraceQL Configurable static fields (#65284)
* TraceQL - configurable static fields for new UI * TraceQL - filter out static fields from Tags section. Added tooltip to static fields * Add more units to duration validation. Improve duration field tooltip with accepted units * Better control of delete button on SearchField * Move new config behind feature toggle * Special title for intrinsic "name" * Fix tests * Move static fields not in the datasource to the Tags section * Start using the useAsync hook in the Tempo TraceQL configuration page to retrieve the tags and datasource * Fix tests * Fix test. Use useAsync to retrieve options in SearchField * Remove ability to set a default value in filter configuration. Removed type from filter, dynamic filters are now any filters not present in the datasource config * Updated the static filters tooltip * Replace useState + useEffect with useMemo for scopedTagpull/65688/head
parent
fb83414b6a
commit
541a03f33b
@ -0,0 +1,59 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React from 'react'; |
||||
import useAsync from 'react-use/lib/useAsync'; |
||||
|
||||
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data'; |
||||
import { getDataSourceSrv } from '@grafana/runtime'; |
||||
import { InlineField, InlineFieldRow, InlineSwitch } from '@grafana/ui'; |
||||
|
||||
import { TempoDatasource } from '../datasource'; |
||||
import { TempoJsonData } from '../types'; |
||||
|
||||
import { TraceQLSearchTags } from './TraceQLSearchTags'; |
||||
|
||||
interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {} |
||||
|
||||
export function TraceQLSearchSettings({ options, onOptionsChange }: Props) { |
||||
const dataSourceSrv = getDataSourceSrv(); |
||||
const fetchDatasource = async () => { |
||||
return (await dataSourceSrv.get({ type: options.type, uid: options.uid })) as TempoDatasource; |
||||
}; |
||||
|
||||
const { value: datasource } = useAsync(fetchDatasource, [dataSourceSrv, options]); |
||||
|
||||
return ( |
||||
<div className={styles.container}> |
||||
<h3 className="page-heading">Tempo search</h3> |
||||
<InlineFieldRow className={styles.row}> |
||||
<InlineField tooltip="Removes the search tab from the query editor" label="Hide search" labelWidth={26}> |
||||
<InlineSwitch |
||||
id="hideSearch" |
||||
value={options.jsonData.search?.hide} |
||||
onChange={(event: React.SyntheticEvent<HTMLInputElement>) => |
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', { |
||||
...options.jsonData.search, |
||||
hide: event.currentTarget.checked, |
||||
}) |
||||
} |
||||
/> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
<InlineFieldRow className={styles.row}> |
||||
<InlineField tooltip="Configures which fields are available in the UI" label="Static filters" labelWidth={26}> |
||||
<TraceQLSearchTags datasource={datasource} options={options} onOptionsChange={onOptionsChange} /> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
const styles = { |
||||
container: css` |
||||
label: container; |
||||
width: 100%; |
||||
`,
|
||||
row: css` |
||||
label: row; |
||||
align-items: baseline; |
||||
`,
|
||||
}; |
@ -0,0 +1,111 @@ |
||||
import React, { useCallback, useEffect } from 'react'; |
||||
import useAsync from 'react-use/lib/useAsync'; |
||||
|
||||
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data'; |
||||
import { Alert } from '@grafana/ui'; |
||||
|
||||
import TagsInput from '../SearchTraceQLEditor/TagsInput'; |
||||
import { replaceAt } from '../SearchTraceQLEditor/utils'; |
||||
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; |
||||
import { TempoDatasource } from '../datasource'; |
||||
import { CompletionProvider } from '../traceql/autocomplete'; |
||||
import { TempoJsonData } from '../types'; |
||||
|
||||
interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> { |
||||
datasource?: TempoDatasource; |
||||
} |
||||
|
||||
export function TraceQLSearchTags({ options, onOptionsChange, datasource }: Props) { |
||||
const fetchTags = async () => { |
||||
if (!datasource) { |
||||
throw new Error('Unable to retrieve datasource'); |
||||
} |
||||
|
||||
try { |
||||
await datasource.languageProvider.start(); |
||||
const tags = datasource.languageProvider.getTags(); |
||||
|
||||
if (tags) { |
||||
// This is needed because the /api/v2/search/tag/${tag}/values API expects "status" and the v1 API expects "status.code"
|
||||
// so Tempo doesn't send anything and we inject it here for the autocomplete
|
||||
if (!tags.find((t) => t === 'status')) { |
||||
tags.push('status'); |
||||
} |
||||
return tags; |
||||
} |
||||
} catch (e) { |
||||
// @ts-ignore
|
||||
throw new Error(`${e.statusText}: ${e.data.error}`); |
||||
} |
||||
return []; |
||||
}; |
||||
|
||||
const { error, loading, value: tags } = useAsync(fetchTags, [datasource, options]); |
||||
|
||||
const updateFilter = useCallback( |
||||
(s: TraceqlFilter) => { |
||||
let copy = options.jsonData.search?.filters; |
||||
copy ||= []; |
||||
const indexOfFilter = copy.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 = replaceAt(copy, indexOfFilter, s); |
||||
} else { |
||||
copy.push(s); |
||||
} |
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', { |
||||
...options.jsonData.search, |
||||
filters: copy, |
||||
}); |
||||
}, |
||||
[onOptionsChange, options] |
||||
); |
||||
|
||||
const deleteFilter = (s: TraceqlFilter) => { |
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', { |
||||
...options.jsonData.search, |
||||
filters: options.jsonData.search?.filters?.filter((f) => f.id !== s.id), |
||||
}); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
if (!options.jsonData.search?.filters) { |
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', { |
||||
...options.jsonData.search, |
||||
filters: [ |
||||
{ |
||||
id: 'service-name', |
||||
tag: 'service.name', |
||||
operator: '=', |
||||
scope: TraceqlSearchScope.Resource, |
||||
}, |
||||
{ id: 'span-name', tag: 'name', operator: '=', scope: TraceqlSearchScope.Span }, |
||||
], |
||||
}); |
||||
} |
||||
}, [onOptionsChange, options]); |
||||
|
||||
return ( |
||||
<> |
||||
{datasource ? ( |
||||
<TagsInput |
||||
updateFilter={updateFilter} |
||||
deleteFilter={deleteFilter} |
||||
filters={options.jsonData.search?.filters || []} |
||||
datasource={datasource} |
||||
setError={() => {}} |
||||
tags={[...CompletionProvider.intrinsics, ...(tags || [])]} |
||||
isTagsLoading={loading} |
||||
hideValues={true} |
||||
/> |
||||
) : ( |
||||
<div>Invalid data source, please create a valid data source and try again</div> |
||||
)} |
||||
{error && ( |
||||
<Alert title={'Unable to fetch TraceQL tags'} severity={'error'} topSpacing={1}> |
||||
{error.message} |
||||
</Alert> |
||||
)} |
||||
</> |
||||
); |
||||
} |
Loading…
Reference in new issue