utils.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import type {FieldDefinitionGetter} from 'sentry/components/searchQueryBuilder/types';
  2. import {
  3. BooleanOperator,
  4. FilterType,
  5. type ParseResult,
  6. type ParseResultToken,
  7. parseSearch,
  8. type SearchConfig,
  9. Token,
  10. type TokenResult,
  11. } from 'sentry/components/searchSyntax/parser';
  12. import {SavedSearchType, type TagCollection} from 'sentry/types/group';
  13. import {FieldValueType} from 'sentry/utils/fields';
  14. export const INTERFACE_TYPE_LOCALSTORAGE_KEY = 'search-query-builder-interface';
  15. function getSearchConfigFromKeys(
  16. keys: TagCollection,
  17. getFieldDefinition: FieldDefinitionGetter
  18. ): Partial<SearchConfig> {
  19. const config = {
  20. textOperatorKeys: new Set<string>(),
  21. booleanKeys: new Set<string>(),
  22. numericKeys: new Set<string>(),
  23. dateKeys: new Set<string>(),
  24. durationKeys: new Set<string>(),
  25. percentageKeys: new Set<string>(),
  26. } satisfies Partial<SearchConfig>;
  27. for (const key in keys) {
  28. const fieldDef = getFieldDefinition(key);
  29. if (!fieldDef) {
  30. continue;
  31. }
  32. if (fieldDef.allowComparisonOperators) {
  33. config.textOperatorKeys.add(key);
  34. }
  35. switch (fieldDef.valueType) {
  36. case FieldValueType.BOOLEAN:
  37. config.booleanKeys.add(key);
  38. break;
  39. case FieldValueType.NUMBER:
  40. case FieldValueType.INTEGER:
  41. case FieldValueType.PERCENTAGE:
  42. config.numericKeys.add(key);
  43. break;
  44. case FieldValueType.DATE:
  45. config.dateKeys.add(key);
  46. break;
  47. case FieldValueType.DURATION:
  48. config.durationKeys.add(key);
  49. break;
  50. default:
  51. break;
  52. }
  53. }
  54. return config;
  55. }
  56. export function parseQueryBuilderValue(
  57. value: string,
  58. getFieldDefinition: FieldDefinitionGetter,
  59. options?: {
  60. filterKeys: TagCollection;
  61. disallowFreeText?: boolean;
  62. disallowLogicalOperators?: boolean;
  63. disallowUnsupportedFilters?: boolean;
  64. disallowWildcard?: boolean;
  65. invalidMessages?: SearchConfig['invalidMessages'];
  66. }
  67. ): ParseResult | null {
  68. return collapseTextTokens(
  69. parseSearch(value || ' ', {
  70. flattenParenGroups: true,
  71. disallowFreeText: options?.disallowFreeText,
  72. validateKeys: options?.disallowUnsupportedFilters,
  73. disallowWildcard: options?.disallowWildcard,
  74. disallowedLogicalOperators: options?.disallowLogicalOperators
  75. ? new Set([BooleanOperator.AND, BooleanOperator.OR])
  76. : undefined,
  77. disallowParens: options?.disallowLogicalOperators,
  78. ...getSearchConfigFromKeys(options?.filterKeys ?? {}, getFieldDefinition),
  79. invalidMessages: options?.invalidMessages,
  80. supportedTags: options?.filterKeys,
  81. })
  82. );
  83. }
  84. /**
  85. * Generates a unique key for the given token.
  86. *
  87. * It's important that the key is as stable as possible. Since we derive tokens
  88. * from the a simple query string, this is difficult to guarantee. The best we
  89. * can do is to use the token type and which iteration of that type it is.
  90. *
  91. * Example for query "is:unresolved foo assignee:me bar":
  92. * Keys: ["freeText:0", "filter:0", "freeText:1" "filter:1", "freeText:2"]
  93. */
  94. export function makeTokenKey(token: ParseResultToken, allTokens: ParseResult | null) {
  95. const tokenTypeIndex =
  96. allTokens?.filter(tk => tk.type === token.type).indexOf(token) ?? 0;
  97. return `${token.type}:${tokenTypeIndex}`;
  98. }
  99. const isSimpleTextToken = (
  100. token: ParseResultToken
  101. ): token is TokenResult<Token.FREE_TEXT> | TokenResult<Token.SPACES> => {
  102. return [Token.FREE_TEXT, Token.SPACES].includes(token.type);
  103. };
  104. /**
  105. * Collapse adjacent FREE_TEXT and SPACES tokens into a single token.
  106. * This is useful for rendering the minimum number of inputs in the UI.
  107. */
  108. function collapseTextTokens(tokens: ParseResult | null) {
  109. if (!tokens) {
  110. return null;
  111. }
  112. return tokens.reduce<ParseResult>((acc, token) => {
  113. // For our purposes, SPACES are equivalent to FREE_TEXT
  114. // Combining them ensures that keys don't change when text is added or removed,
  115. // which would cause the cursor to jump around.
  116. if (isSimpleTextToken(token)) {
  117. token.type = Token.FREE_TEXT;
  118. }
  119. if (acc.length === 0) {
  120. return [token];
  121. }
  122. const lastToken = acc[acc.length - 1];
  123. if (isSimpleTextToken(token) && isSimpleTextToken(lastToken)) {
  124. const freeTextToken = lastToken as TokenResult<Token.FREE_TEXT>;
  125. freeTextToken.value += token.value;
  126. freeTextToken.text += token.text;
  127. freeTextToken.location.end = token.location.end;
  128. if (token.type === Token.FREE_TEXT) {
  129. freeTextToken.quoted = freeTextToken.quoted || token.quoted;
  130. freeTextToken.invalid = freeTextToken.invalid ?? token.invalid;
  131. }
  132. return acc;
  133. }
  134. return [...acc, token];
  135. }, []);
  136. }
  137. export function tokenIsInvalid(token: TokenResult<Token>) {
  138. if (
  139. token.type !== Token.FILTER &&
  140. token.type !== Token.FREE_TEXT &&
  141. token.type !== Token.LOGIC_BOOLEAN
  142. ) {
  143. return false;
  144. }
  145. return Boolean(token.invalid);
  146. }
  147. export function queryIsValid(parsedQuery: ParseResult | null) {
  148. if (!parsedQuery) {
  149. return false;
  150. }
  151. return !parsedQuery.some(tokenIsInvalid);
  152. }
  153. export function isDateToken(token: TokenResult<Token.FILTER>) {
  154. return [FilterType.DATE, FilterType.RELATIVE_DATE, FilterType.SPECIFIC_DATE].includes(
  155. token.filter
  156. );
  157. }
  158. export function recentSearchTypeToLabel(type: SavedSearchType | undefined) {
  159. switch (type) {
  160. case SavedSearchType.ISSUE:
  161. return 'issues';
  162. case SavedSearchType.EVENT:
  163. return 'events';
  164. case SavedSearchType.METRIC:
  165. return 'metrics';
  166. case SavedSearchType.REPLAY:
  167. return 'replays';
  168. case SavedSearchType.SESSION:
  169. return 'sessions';
  170. case SavedSearchType.SPAN:
  171. return 'spans';
  172. default:
  173. return 'none';
  174. }
  175. }