LokiQueryFieldForm.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. // Libraries
  2. import React from 'react';
  3. // @ts-ignore
  4. import Cascader from 'rc-cascader';
  5. // @ts-ignore
  6. import PluginPrism from 'slate-prism';
  7. // Components
  8. import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
  9. // Utils & Services
  10. // dom also includes Element polyfills
  11. import BracesPlugin from 'app/features/explore/slate-plugins/braces';
  12. // Types
  13. import { LokiQuery } from '../types';
  14. import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
  15. import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus, DOMUtil } from '@grafana/ui';
  16. import { AbsoluteTimeRange } from '@grafana/data';
  17. function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
  18. if (datasourceStatus === DataSourceStatus.Disconnected) {
  19. return '(Disconnected)';
  20. }
  21. if (!hasSyntax) {
  22. return 'Loading labels...';
  23. }
  24. if (!hasLogLabels) {
  25. return '(No labels found)';
  26. }
  27. return 'Log labels';
  28. }
  29. function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
  30. // Modify suggestion based on context
  31. switch (typeaheadContext) {
  32. case 'context-labels': {
  33. const nextChar = DOMUtil.getNextCharacter();
  34. if (!nextChar || nextChar === '}' || nextChar === ',') {
  35. suggestion += '=';
  36. }
  37. break;
  38. }
  39. case 'context-label-values': {
  40. // Always add quotes and remove existing ones instead
  41. if (!typeaheadText.match(/^(!?=~?"|")/)) {
  42. suggestion = `"${suggestion}`;
  43. }
  44. if (DOMUtil.getNextCharacter() !== '"') {
  45. suggestion = `${suggestion}"`;
  46. }
  47. break;
  48. }
  49. default:
  50. }
  51. return suggestion;
  52. }
  53. export interface CascaderOption {
  54. label: string;
  55. value: string;
  56. children?: CascaderOption[];
  57. disabled?: boolean;
  58. }
  59. export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<DataSourceApi<LokiQuery>, LokiQuery> {
  60. history: HistoryItem[];
  61. syntax: any;
  62. logLabelOptions: any[];
  63. syntaxLoaded: any;
  64. absoluteRange: AbsoluteTimeRange;
  65. onLoadOptions: (selectedOptions: CascaderOption[]) => void;
  66. onLabelsRefresh?: () => void;
  67. }
  68. export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormProps> {
  69. plugins: any[];
  70. modifiedSearch: string;
  71. modifiedQuery: string;
  72. constructor(props: LokiQueryFieldFormProps, context: React.Context<any>) {
  73. super(props, context);
  74. this.plugins = [
  75. BracesPlugin(),
  76. PluginPrism({
  77. onlyIn: (node: any) => node.type === 'code_block',
  78. getSyntax: (node: any) => 'promql',
  79. }),
  80. ];
  81. }
  82. loadOptions = (selectedOptions: CascaderOption[]) => {
  83. this.props.onLoadOptions(selectedOptions);
  84. };
  85. onChangeLogLabels = (values: string[], selectedOptions: CascaderOption[]) => {
  86. if (selectedOptions.length === 2) {
  87. const key = selectedOptions[0].value;
  88. const value = selectedOptions[1].value;
  89. const query = `{${key}="${value}"}`;
  90. this.onChangeQuery(query, true);
  91. }
  92. };
  93. onChangeQuery = (value: string, override?: boolean) => {
  94. // Send text change to parent
  95. const { query, onChange, onRunQuery } = this.props;
  96. if (onChange) {
  97. const nextQuery = { ...query, expr: value };
  98. onChange(nextQuery);
  99. if (override && onRunQuery) {
  100. onRunQuery();
  101. }
  102. }
  103. };
  104. onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
  105. const { datasource } = this.props;
  106. if (!datasource.languageProvider) {
  107. return { suggestions: [] };
  108. }
  109. const { history, absoluteRange } = this.props;
  110. const { prefix, text, value, wrapperNode } = typeahead;
  111. // Get DOM-dependent context
  112. const wrapperClasses = Array.from(wrapperNode.classList);
  113. const labelKeyNode = DOMUtil.getPreviousCousin(wrapperNode, '.attr-name');
  114. const labelKey = labelKeyNode && labelKeyNode.textContent;
  115. const nextChar = DOMUtil.getNextCharacter();
  116. const result = datasource.languageProvider.provideCompletionItems(
  117. { text, value, prefix, wrapperClasses, labelKey },
  118. { history, absoluteRange }
  119. );
  120. console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
  121. return result;
  122. };
  123. render() {
  124. const {
  125. queryResponse,
  126. query,
  127. syntaxLoaded,
  128. logLabelOptions,
  129. onLoadOptions,
  130. onLabelsRefresh,
  131. datasource,
  132. datasourceStatus,
  133. } = this.props;
  134. const cleanText = datasource.languageProvider ? datasource.languageProvider.cleanText : undefined;
  135. const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
  136. const chooserText = getChooserText(syntaxLoaded, hasLogLabels, datasourceStatus);
  137. const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
  138. const showError = queryResponse && queryResponse.error && queryResponse.error.refId === query.refId;
  139. return (
  140. <>
  141. <div className="gf-form-inline">
  142. <div className="gf-form">
  143. <Cascader
  144. options={logLabelOptions}
  145. onChange={this.onChangeLogLabels}
  146. loadData={onLoadOptions}
  147. expandIcon={null}
  148. onPopupVisibleChange={(isVisible: boolean) => {
  149. if (isVisible && onLabelsRefresh) {
  150. onLabelsRefresh();
  151. }
  152. }}
  153. >
  154. <button className="gf-form-label gf-form-label--btn" disabled={buttonDisabled}>
  155. {chooserText} <i className="fa fa-caret-down" />
  156. </button>
  157. </Cascader>
  158. </div>
  159. <div className="gf-form gf-form--grow">
  160. <QueryField
  161. additionalPlugins={this.plugins}
  162. cleanText={cleanText}
  163. initialQuery={query.expr}
  164. onTypeahead={this.onTypeahead}
  165. onWillApplySuggestion={willApplySuggestion}
  166. onChange={this.onChangeQuery}
  167. onRunQuery={this.props.onRunQuery}
  168. placeholder="Enter a Loki query"
  169. portalOrigin="loki"
  170. syntaxLoaded={syntaxLoaded}
  171. />
  172. </div>
  173. </div>
  174. <div>
  175. {showError ? <div className="prom-query-field-info text-error">{queryResponse.error.message}</div> : null}
  176. </div>
  177. </>
  178. );
  179. }
  180. }