grafana/public/app/plugins/datasource/loki/components/LokiQueryField.tsx

267 lines
7.8 KiB
TypeScript
Raw Normal View History

// Libraries
import React from 'react';
import Cascader from 'rc-cascader';
import PluginPrism from 'slate-prism';
import Prism from 'prismjs';
// Components
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
// Utils & Services
// dom also includes Element polyfills
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
// Types
import { LokiQuery } from '../types';
2019-02-01 04:55:01 -06:00
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
import { makePromiseCancelable, CancelablePromise } from 'app/core/utils/CancelablePromise';
2019-02-01 04:55:01 -06:00
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
const PRISM_SYNTAX = 'promql';
function getChooserText(hasSytax, hasLogLabels) {
if (!hasSytax) {
return 'Loading labels...';
}
if (!hasLogLabels) {
return '(No labels found)';
}
return 'Log labels';
}
2018-12-09 16:39:25 -06:00
export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
// Modify suggestion based on context
switch (typeaheadContext) {
case 'context-labels': {
const nextChar = getNextCharacter();
if (!nextChar || nextChar === '}' || nextChar === ',') {
suggestion += '=';
}
break;
}
case 'context-label-values': {
// Always add quotes and remove existing ones instead
if (!typeaheadText.match(/^(!?=~?"|")/)) {
suggestion = `"${suggestion}`;
}
if (getNextCharacter() !== '"') {
suggestion = `${suggestion}"`;
}
break;
}
default:
}
return suggestion;
}
interface CascaderOption {
label: string;
value: string;
children?: CascaderOption[];
disabled?: boolean;
}
2019-02-01 04:55:01 -06:00
interface LokiQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceApi, LokiQuery> {
history: HistoryItem[];
}
2018-12-05 16:13:57 -06:00
interface LokiQueryFieldState {
logLabelOptions: any[];
syntaxLoaded: boolean;
}
export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
plugins: any[];
pluginsSearch: any[];
languageProvider: any;
modifiedSearch: string;
modifiedQuery: string;
languageProviderInitializationPromise: CancelablePromise<any>;
2018-12-05 16:13:57 -06:00
constructor(props: LokiQueryFieldProps, context) {
super(props, context);
if (props.datasource.languageProvider) {
this.languageProvider = props.datasource.languageProvider;
}
this.plugins = [
BracesPlugin(),
2019-02-01 04:55:01 -06:00
RunnerPlugin({ handler: props.onExecuteQuery }),
PluginPrism({
onlyIn: node => node.type === 'code_block',
getSyntax: node => 'promql',
}),
];
2019-02-01 04:55:01 -06:00
this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })];
this.state = {
logLabelOptions: [],
syntaxLoaded: false,
};
}
componentDidMount() {
if (this.languageProvider) {
this.languageProviderInitializationPromise = makePromiseCancelable(this.languageProvider.start());
this.languageProviderInitializationPromise.promise
.then(remaining => {
remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {}));
})
.then(() => this.onUpdateLanguage())
.catch(({ isCanceled }) => {
if (isCanceled) {
console.warn('LokiQueryField has unmounted, language provider intialization was canceled');
}
});
}
}
componentWillUnmount() {
if (this.languageProviderInitializationPromise) {
this.languageProviderInitializationPromise.cancel();
}
}
loadOptions = (selectedOptions: CascaderOption[]) => {
const targetOption = selectedOptions[selectedOptions.length - 1];
this.setState(state => {
const nextOptions = state.logLabelOptions.map(option => {
if (option.value === targetOption.value) {
return {
...option,
loading: true,
};
}
return option;
});
return { logLabelOptions: nextOptions };
});
this.languageProvider
.fetchLabelValues(targetOption.value)
.then(this.onUpdateLanguage)
.catch(() => {});
};
onChangeLogLabels = (values: string[], selectedOptions: CascaderOption[]) => {
if (selectedOptions.length === 2) {
const key = selectedOptions[0].value;
const value = selectedOptions[1].value;
const query = `{${key}="${value}"}`;
this.onChangeQuery(query, true);
}
};
onChangeQuery = (value: string, override?: boolean) => {
// Send text change to parent
2019-02-04 23:19:40 -06:00
const { query, onQueryChange, onExecuteQuery } = this.props;
if (onQueryChange) {
2019-02-04 23:19:40 -06:00
const nextQuery = { ...query, expr: value };
onQueryChange(nextQuery);
2019-02-01 04:55:01 -06:00
if (override && onExecuteQuery) {
onExecuteQuery();
}
}
};
onClickHintFix = () => {
2019-02-01 05:54:16 -06:00
const { hint, onExecuteHint } = this.props;
if (onExecuteHint && hint && hint.fix) {
onExecuteHint(hint.fix.action);
}
};
onUpdateLanguage = () => {
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
const { logLabelOptions } = this.languageProvider;
this.setState({
logLabelOptions,
syntaxLoaded: true,
});
};
onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
if (!this.languageProvider) {
return { suggestions: [] };
}
const { history } = this.props;
const { prefix, text, value, wrapperNode } = typeahead;
// Get DOM-dependent context
const wrapperClasses = Array.from(wrapperNode.classList);
const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name');
const labelKey = labelKeyNode && labelKeyNode.textContent;
const nextChar = getNextCharacter();
const result = this.languageProvider.provideCompletionItems(
{ text, value, prefix, wrapperClasses, labelKey },
{ history }
);
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
return result;
};
render() {
2019-02-04 23:19:40 -06:00
const { error, hint, query } = this.props;
const { logLabelOptions, syntaxLoaded } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
const chooserText = getChooserText(syntaxLoaded, hasLogLabels);
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<Cascader options={logLabelOptions} onChange={this.onChangeLogLabels} loadData={this.loadOptions}>
<button className="gf-form-label gf-form-label--btn" disabled={!syntaxLoaded}>
{chooserText} <i className="fa fa-caret-down" />
</button>
</Cascader>
</div>
<div className="gf-form gf-form--grow">
<QueryField
additionalPlugins={this.plugins}
cleanText={cleanText}
2019-02-04 23:19:40 -06:00
initialQuery={query.expr}
onTypeahead={this.onTypeahead}
onWillApplySuggestion={willApplySuggestion}
2019-02-01 04:55:01 -06:00
onQueryChange={this.onChangeQuery}
onExecuteQuery={this.props.onExecuteQuery}
placeholder="Enter a Loki query"
portalOrigin="loki"
syntaxLoaded={syntaxLoaded}
/>
</div>
</div>
<div>
{error ? <div className="prom-query-field-info text-error">{error}</div> : null}
{hint ? (
<div className="prom-query-field-info text-warning">
{hint.label}{' '}
{hint.fix ? (
<a className="text-link muted" onClick={this.onClickHintFix}>
{hint.fix.label}
</a>
) : null}
</div>
) : null}
</div>
</>
);
}
}
2018-12-05 16:13:57 -06:00
export default LokiQueryField;