LokiQueryField.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import { LanguageMap, languages as prismLanguages } from 'prismjs';
  2. import React, { ReactNode } from 'react';
  3. import { Plugin, Node } from 'slate';
  4. import { QueryEditorProps } from '@grafana/data';
  5. import { reportInteraction } from '@grafana/runtime';
  6. import {
  7. SlatePrism,
  8. TypeaheadOutput,
  9. SuggestionsState,
  10. QueryField,
  11. TypeaheadInput,
  12. BracesPlugin,
  13. DOMUtil,
  14. Icon,
  15. } from '@grafana/ui';
  16. import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
  17. import { LokiDatasource } from '../datasource';
  18. import LokiLanguageProvider from '../language_provider';
  19. import { escapeLabelValueInSelector, shouldRefreshLabels } from '../language_utils';
  20. import { LokiQuery, LokiOptions } from '../types';
  21. import { LokiLabelBrowser } from './LokiLabelBrowser';
  22. const LAST_USED_LABELS_KEY = 'grafana.datasources.loki.browser.labels';
  23. function getChooserText(hasSyntax: boolean, hasLogLabels: boolean) {
  24. if (!hasSyntax) {
  25. return 'Loading labels...';
  26. }
  27. if (!hasLogLabels) {
  28. return '(No logs found)';
  29. }
  30. return 'Log browser';
  31. }
  32. function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: SuggestionsState): string {
  33. // Modify suggestion based on context
  34. switch (typeaheadContext) {
  35. case 'context-labels': {
  36. const nextChar = DOMUtil.getNextCharacter();
  37. if (!nextChar || nextChar === '}' || nextChar === ',') {
  38. suggestion += '=';
  39. }
  40. break;
  41. }
  42. case 'context-label-values': {
  43. // Always add quotes and remove existing ones instead
  44. let suggestionModified = '';
  45. if (!typeaheadText.match(/^(!?=~?"|")/)) {
  46. suggestionModified = '"';
  47. }
  48. suggestionModified += escapeLabelValueInSelector(suggestion, typeaheadText);
  49. if (DOMUtil.getNextCharacter() !== '"') {
  50. suggestionModified += '"';
  51. }
  52. suggestion = suggestionModified;
  53. break;
  54. }
  55. default:
  56. }
  57. return suggestion;
  58. }
  59. export interface LokiQueryFieldProps extends QueryEditorProps<LokiDatasource, LokiQuery, LokiOptions> {
  60. ExtraFieldElement?: ReactNode;
  61. placeholder?: string;
  62. 'data-testid'?: string;
  63. }
  64. interface LokiQueryFieldState {
  65. labelsLoaded: boolean;
  66. labelBrowserVisible: boolean;
  67. }
  68. export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
  69. plugins: Plugin[];
  70. _isMounted = false;
  71. constructor(props: LokiQueryFieldProps) {
  72. super(props);
  73. this.state = { labelsLoaded: false, labelBrowserVisible: false };
  74. this.plugins = [
  75. BracesPlugin(),
  76. SlatePrism(
  77. {
  78. onlyIn: (node: Node) => node.object === 'block' && node.type === 'code_block',
  79. getSyntax: (node: Node) => 'logql',
  80. },
  81. { ...(prismLanguages as LanguageMap), logql: this.props.datasource.languageProvider.getSyntax() }
  82. ),
  83. ];
  84. }
  85. async componentDidMount() {
  86. this._isMounted = true;
  87. await this.props.datasource.languageProvider.start();
  88. if (this._isMounted) {
  89. this.setState({ labelsLoaded: true });
  90. }
  91. }
  92. componentWillUnmount() {
  93. this._isMounted = false;
  94. }
  95. componentDidUpdate(prevProps: LokiQueryFieldProps) {
  96. const {
  97. range,
  98. datasource: { languageProvider },
  99. } = this.props;
  100. const refreshLabels = shouldRefreshLabels(range, prevProps.range);
  101. // We want to refresh labels when range changes (we round up intervals to a minute)
  102. if (refreshLabels) {
  103. languageProvider.fetchLabels();
  104. }
  105. }
  106. onChangeLabelBrowser = (selector: string) => {
  107. this.onChangeQuery(selector, true);
  108. this.setState({ labelBrowserVisible: false });
  109. };
  110. onChangeQuery = (value: string, override?: boolean) => {
  111. // Send text change to parent
  112. const { query, onChange, onRunQuery } = this.props;
  113. if (onChange) {
  114. const nextQuery = { ...query, expr: value };
  115. onChange(nextQuery);
  116. if (override && onRunQuery) {
  117. onRunQuery();
  118. }
  119. }
  120. };
  121. onClickChooserButton = () => {
  122. if (!this.state.labelBrowserVisible) {
  123. reportInteraction('grafana_loki_log_browser_opened', {
  124. app: this.props.app,
  125. });
  126. } else {
  127. reportInteraction('grafana_loki_log_browser_closed', {
  128. app: this.props.app,
  129. closeType: 'logBrowserButton',
  130. });
  131. }
  132. this.setState((state) => ({ labelBrowserVisible: !state.labelBrowserVisible }));
  133. };
  134. onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
  135. const { datasource } = this.props;
  136. if (!datasource.languageProvider) {
  137. return { suggestions: [] };
  138. }
  139. const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
  140. const { history } = this.props;
  141. const { prefix, text, value, wrapperClasses, labelKey } = typeahead;
  142. const result = await lokiLanguageProvider.provideCompletionItems(
  143. { text, value, prefix, wrapperClasses, labelKey },
  144. { history }
  145. );
  146. return result;
  147. };
  148. render() {
  149. const {
  150. ExtraFieldElement,
  151. query,
  152. app,
  153. datasource,
  154. placeholder = 'Enter a Loki query (run with Shift+Enter)',
  155. } = this.props;
  156. const { labelsLoaded, labelBrowserVisible } = this.state;
  157. const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
  158. const cleanText = datasource.languageProvider ? lokiLanguageProvider.cleanText : undefined;
  159. const hasLogLabels = lokiLanguageProvider.getLabelKeys().length > 0;
  160. const chooserText = getChooserText(labelsLoaded, hasLogLabels);
  161. const buttonDisabled = !(labelsLoaded && hasLogLabels);
  162. return (
  163. <LocalStorageValueProvider<string[]> storageKey={LAST_USED_LABELS_KEY} defaultValue={[]}>
  164. {(lastUsedLabels, onLastUsedLabelsSave, onLastUsedLabelsDelete) => {
  165. return (
  166. <>
  167. <div
  168. className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1"
  169. data-testid={this.props['data-testid']}
  170. >
  171. <button
  172. className="gf-form-label query-keyword pointer"
  173. onClick={this.onClickChooserButton}
  174. disabled={buttonDisabled}
  175. >
  176. {chooserText}
  177. <Icon name={labelBrowserVisible ? 'angle-down' : 'angle-right'} />
  178. </button>
  179. <div className="gf-form gf-form--grow flex-shrink-1 min-width-15">
  180. <QueryField
  181. additionalPlugins={this.plugins}
  182. cleanText={cleanText}
  183. query={query.expr}
  184. onTypeahead={this.onTypeahead}
  185. onWillApplySuggestion={willApplySuggestion}
  186. onChange={this.onChangeQuery}
  187. onBlur={this.props.onBlur}
  188. onRunQuery={this.props.onRunQuery}
  189. placeholder={placeholder}
  190. portalOrigin="loki"
  191. />
  192. </div>
  193. </div>
  194. {labelBrowserVisible && (
  195. <div className="gf-form">
  196. <LokiLabelBrowser
  197. languageProvider={lokiLanguageProvider}
  198. onChange={this.onChangeLabelBrowser}
  199. lastUsedLabels={lastUsedLabels || []}
  200. storeLastUsedLabels={onLastUsedLabelsSave}
  201. deleteLastUsedLabels={onLastUsedLabelsDelete}
  202. app={app}
  203. />
  204. </div>
  205. )}
  206. {ExtraFieldElement}
  207. </>
  208. );
  209. }}
  210. </LocalStorageValueProvider>
  211. );
  212. }
  213. }