You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
280 lines
9.0 KiB
280 lines
9.0 KiB
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)};
|
|
`,
|
|
});
|
|
|