LokiQueryFieldForm.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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 { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
  12. import BracesPlugin from 'app/features/explore/slate-plugins/braces';
  13. // Types
  14. import { LokiQuery } from '../types';
  15. import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
  16. import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus } from '@grafana/ui';
  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 = 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 (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. onLoadOptions: (selectedOptions: CascaderOption[]) => void;
  65. onLabelsRefresh?: () => void;
  66. }
  67. export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormProps> {
  68. plugins: any[];
  69. modifiedSearch: string;
  70. modifiedQuery: string;
  71. constructor(props: LokiQueryFieldFormProps, context: React.Context<any>) {
  72. super(props, context);
  73. this.plugins = [
  74. BracesPlugin(),
  75. PluginPrism({
  76. onlyIn: (node: any) => node.type === 'code_block',
  77. getSyntax: (node: any) => 'promql',
  78. }),
  79. ];
  80. }
  81. loadOptions = (selectedOptions: CascaderOption[]) => {
  82. this.props.onLoadOptions(selectedOptions);
  83. };
  84. onChangeLogLabels = (values: string[], selectedOptions: CascaderOption[]) => {
  85. if (selectedOptions.length === 2) {
  86. const key = selectedOptions[0].value;
  87. const value = selectedOptions[1].value;
  88. const query = `{${key}="${value}"}`;
  89. this.onChangeQuery(query, true);
  90. }
  91. };
  92. onChangeQuery = (value: string, override?: boolean) => {
  93. // Send text change to parent
  94. const { query, onChange, onRunQuery } = this.props;
  95. if (onChange) {
  96. const nextQuery = { ...query, expr: value };
  97. onChange(nextQuery);
  98. if (override && onRunQuery) {
  99. onRunQuery();
  100. }
  101. }
  102. };
  103. onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
  104. const { datasource } = this.props;
  105. if (!datasource.languageProvider) {
  106. return { suggestions: [] };
  107. }
  108. const { history } = this.props;
  109. const { prefix, text, value, wrapperNode } = typeahead;
  110. // Get DOM-dependent context
  111. const wrapperClasses = Array.from(wrapperNode.classList);
  112. const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name');
  113. const labelKey = labelKeyNode && labelKeyNode.textContent;
  114. const nextChar = getNextCharacter();
  115. const result = datasource.languageProvider.provideCompletionItems(
  116. { text, value, prefix, wrapperClasses, labelKey },
  117. { history }
  118. );
  119. console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
  120. return result;
  121. };
  122. render() {
  123. const {
  124. queryResponse,
  125. query,
  126. syntaxLoaded,
  127. logLabelOptions,
  128. onLoadOptions,
  129. onLabelsRefresh,
  130. datasource,
  131. datasourceStatus,
  132. } = this.props;
  133. const cleanText = datasource.languageProvider ? datasource.languageProvider.cleanText : undefined;
  134. const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
  135. const chooserText = getChooserText(syntaxLoaded, hasLogLabels, datasourceStatus);
  136. const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
  137. return (
  138. <>
  139. <div className="gf-form-inline">
  140. <div className="gf-form">
  141. <Cascader
  142. options={logLabelOptions}
  143. onChange={this.onChangeLogLabels}
  144. loadData={onLoadOptions}
  145. onPopupVisibleChange={(isVisible: boolean) => {
  146. if (isVisible && onLabelsRefresh) {
  147. onLabelsRefresh();
  148. }
  149. }}
  150. >
  151. <button className="gf-form-label gf-form-label--btn" disabled={buttonDisabled}>
  152. {chooserText} <i className="fa fa-caret-down" />
  153. </button>
  154. </Cascader>
  155. </div>
  156. <div className="gf-form gf-form--grow">
  157. <QueryField
  158. additionalPlugins={this.plugins}
  159. cleanText={cleanText}
  160. initialQuery={query.expr}
  161. onTypeahead={this.onTypeahead}
  162. onWillApplySuggestion={willApplySuggestion}
  163. onChange={this.onChangeQuery}
  164. onRunQuery={this.props.onRunQuery}
  165. placeholder="Enter a Loki query"
  166. portalOrigin="loki"
  167. syntaxLoaded={syntaxLoaded}
  168. />
  169. </div>
  170. </div>
  171. <div>
  172. {queryResponse && queryResponse.error ? (
  173. <div className="prom-query-field-info text-error">{queryResponse.error.message}</div>
  174. ) : null}
  175. </div>
  176. </>
  177. );
  178. }
  179. }