helpers.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { clone } from 'lodash';
  2. import { createErrorNotification } from '../../../../core/copy/appNotification';
  3. import { notifyApp } from '../../../../core/reducers/appNotification';
  4. import { dispatch } from '../../../../store/store';
  5. import { FuncInstance } from '../gfunc';
  6. import { GraphiteQuery, GraphiteTagOperator } from '../types';
  7. import { GraphiteQueryEditorState } from './store';
  8. /**
  9. * Helpers used by reducers and providers. They modify state object directly so should operate on a copy of the state.
  10. */
  11. export const GRAPHITE_TAG_OPERATORS: GraphiteTagOperator[] = ['=', '!=', '=~', '!=~'];
  12. /**
  13. * Tag names and metric names are displayed in a single dropdown. This prefix is used to
  14. * distinguish both in the UI.
  15. */
  16. export const TAG_PREFIX = 'tag: ';
  17. /**
  18. * Create new AST based on new query.
  19. * Build segments from parsed metric name and functions.
  20. */
  21. export async function parseTarget(state: GraphiteQueryEditorState): Promise<void> {
  22. state.queryModel.parseTarget();
  23. await buildSegments(state);
  24. }
  25. /**
  26. * Create segments out of the current metric path + add "select metrics" if it's possible to add more to the path
  27. */
  28. export async function buildSegments(state: GraphiteQueryEditorState, modifyLastSegment = true): Promise<void> {
  29. // Start with a shallow copy from the model, then check if "select metric" segment should be added at the end
  30. state.segments = clone(state.queryModel.segments);
  31. const checkOtherSegmentsIndex = state.queryModel.checkOtherSegmentsIndex || 0;
  32. await checkOtherSegments(state, checkOtherSegmentsIndex, modifyLastSegment);
  33. }
  34. /**
  35. * Add "select metric" segment at the end
  36. */
  37. export function addSelectMetricSegment(state: GraphiteQueryEditorState): void {
  38. state.queryModel.addSelectMetricSegment();
  39. state.segments.push({ value: 'select metric', fake: true });
  40. }
  41. /**
  42. * Validates the state after adding or changing a segment:
  43. * - adds "select metric" only when more segments can be added to the metric name
  44. * - check if subsequent segments are still valid if in-between segment changes and
  45. * removes invalid segments.
  46. */
  47. export async function checkOtherSegments(
  48. state: GraphiteQueryEditorState,
  49. fromIndex: number,
  50. modifyLastSegment = true
  51. ): Promise<void> {
  52. if (state.queryModel.segments.length === 1 && state.queryModel.segments[0].type === 'series-ref') {
  53. return;
  54. }
  55. if (fromIndex === 0) {
  56. addSelectMetricSegment(state);
  57. return;
  58. }
  59. const path = state.queryModel.getSegmentPathUpTo(fromIndex + 1);
  60. if (path === '') {
  61. return;
  62. }
  63. try {
  64. const segments = await state.datasource.metricFindQuery(path);
  65. if (segments.length === 0) {
  66. if (path !== '' && modifyLastSegment) {
  67. state.queryModel.segments = state.queryModel.segments.splice(0, fromIndex);
  68. state.segments = state.segments.splice(0, fromIndex);
  69. addSelectMetricSegment(state);
  70. }
  71. } else if (segments[0].expandable) {
  72. if (state.segments.length === fromIndex) {
  73. addSelectMetricSegment(state);
  74. } else {
  75. await checkOtherSegments(state, fromIndex + 1);
  76. }
  77. }
  78. } catch (err) {
  79. handleMetricsAutoCompleteError(state, err);
  80. }
  81. }
  82. export function spliceSegments(state: GraphiteQueryEditorState, index: number): void {
  83. state.segments = state.segments.splice(0, index);
  84. state.queryModel.segments = state.queryModel.segments.splice(0, index);
  85. }
  86. export function emptySegments(state: GraphiteQueryEditorState): void {
  87. state.queryModel.segments = [];
  88. state.segments = [];
  89. }
  90. /**
  91. * When seriesByTag function is added the UI changes it's state and only tags can be added from now.
  92. */
  93. export async function addSeriesByTagFunc(state: GraphiteQueryEditorState, tag: string): Promise<void> {
  94. const newFunc = state.datasource.createFuncInstance('seriesByTag', {
  95. withDefaultParams: false,
  96. });
  97. const tagParam = `${tag}=`;
  98. newFunc.params = [tagParam];
  99. state.queryModel.addFunction(newFunc);
  100. newFunc.added = true;
  101. emptySegments(state);
  102. handleTargetChanged(state);
  103. await parseTarget(state);
  104. }
  105. export function smartlyHandleNewAliasByNode(state: GraphiteQueryEditorState, func: FuncInstance): void {
  106. if (func.def.name !== 'aliasByNode') {
  107. return;
  108. }
  109. for (let i = 0; i < state.segments.length; i++) {
  110. if (state.segments[i].value.indexOf('*') >= 0) {
  111. func.params[0] = i;
  112. func.added = false;
  113. handleTargetChanged(state);
  114. return;
  115. }
  116. }
  117. }
  118. /**
  119. * Pauses running the query to allow selecting tag value. This is to prevent getting errors if the query is run
  120. * for a tag with no selected value.
  121. */
  122. export function pause(state: GraphiteQueryEditorState): void {
  123. state.paused = true;
  124. }
  125. export function removeTagPrefix(value: string): string {
  126. return value.replace(TAG_PREFIX, '');
  127. }
  128. export function handleTargetChanged(state: GraphiteQueryEditorState): void {
  129. if (state.queryModel.error) {
  130. return;
  131. }
  132. const oldTarget = state.queryModel.target.target;
  133. // Interpolate from other queries:
  134. // Because of mixed data sources the list may contain queries for non-Graphite data sources. To ensure a valid query
  135. // is used for interpolation we should check required properties are passed though in theory it allows to interpolate
  136. // with queries that contain "target" property as well.
  137. state.queryModel.updateModelTarget(
  138. (state.queries || []).filter((query) => 'target' in query && typeof (query as GraphiteQuery).target === 'string')
  139. );
  140. if (state.queryModel.target.target !== oldTarget && !state.paused) {
  141. state.refresh();
  142. }
  143. }
  144. /**
  145. * When metrics autocomplete fails - the error is shown, but only once per page view
  146. */
  147. export function handleMetricsAutoCompleteError(
  148. state: GraphiteQueryEditorState,
  149. error: Error
  150. ): GraphiteQueryEditorState {
  151. if (!state.metricAutoCompleteErrorShown) {
  152. state.metricAutoCompleteErrorShown = true;
  153. dispatch(notifyApp(createErrorNotification(`Fetching metrics failed: ${error.message}.`)));
  154. }
  155. return state;
  156. }
  157. /**
  158. * When tags autocomplete fails - the error is shown, but only once per page view
  159. */
  160. export function handleTagsAutoCompleteError(state: GraphiteQueryEditorState, error: Error): GraphiteQueryEditorState {
  161. if (!state.tagsAutoCompleteErrorShown) {
  162. state.tagsAutoCompleteErrorShown = true;
  163. dispatch(notifyApp(createErrorNotification(`Fetching tags failed: ${error.message}.`)));
  164. }
  165. return state;
  166. }