|
|
@ -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> |
|
|
|
|
|
|
|
); |
|
|
|
); |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|