index.stories.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. import {Fragment, useState} from 'react';
  2. import MultipleCheckbox from 'sentry/components/forms/controls/multipleCheckbox';
  3. import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';
  4. import type {FilterKeySection} from 'sentry/components/searchQueryBuilder/types';
  5. import {InvalidReason} from 'sentry/components/searchSyntax/parser';
  6. import {ItemType} from 'sentry/components/smartSearchBar/types';
  7. import JSXNode from 'sentry/components/stories/jsxNode';
  8. import JSXProperty from 'sentry/components/stories/jsxProperty';
  9. import storyBook from 'sentry/stories/storyBook';
  10. import type {TagCollection} from 'sentry/types/group';
  11. import {
  12. FieldKey,
  13. FieldKind,
  14. FieldValueType,
  15. MobileVital,
  16. WebVital,
  17. } from 'sentry/utils/fields';
  18. const FILTER_KEYS: TagCollection = {
  19. [FieldKey.ASSIGNED]: {
  20. key: FieldKey.ASSIGNED,
  21. name: 'Assigned To',
  22. kind: FieldKind.FIELD,
  23. predefined: true,
  24. values: [
  25. {
  26. title: 'Suggested',
  27. type: 'header',
  28. icon: null,
  29. children: [{value: 'me'}, {value: 'unassigned'}],
  30. },
  31. {
  32. title: 'All',
  33. type: 'header',
  34. icon: null,
  35. children: [{value: 'person1@sentry.io'}, {value: 'person2@sentry.io'}],
  36. },
  37. ],
  38. },
  39. [FieldKey.BROWSER_NAME]: {
  40. key: FieldKey.BROWSER_NAME,
  41. name: 'Browser Name',
  42. kind: FieldKind.FIELD,
  43. predefined: true,
  44. values: ['Chrome', 'Firefox', 'Safari', 'Edge'],
  45. },
  46. [FieldKey.IS]: {
  47. key: FieldKey.IS,
  48. name: 'is',
  49. predefined: true,
  50. values: ['resolved', 'unresolved', 'ignored'],
  51. },
  52. [FieldKey.LAST_SEEN]: {
  53. key: FieldKey.LAST_SEEN,
  54. name: 'lastSeen',
  55. kind: FieldKind.FIELD,
  56. },
  57. [FieldKey.TIMES_SEEN]: {
  58. key: FieldKey.TIMES_SEEN,
  59. name: 'timesSeen',
  60. kind: FieldKind.FIELD,
  61. },
  62. [WebVital.LCP]: {
  63. key: WebVital.LCP,
  64. name: 'lcp',
  65. kind: FieldKind.FIELD,
  66. },
  67. [MobileVital.FRAMES_SLOW_RATE]: {
  68. key: MobileVital.FRAMES_SLOW_RATE,
  69. name: 'framesSlowRate',
  70. kind: FieldKind.FIELD,
  71. },
  72. custom_tag_name: {
  73. key: 'custom_tag_name',
  74. name: 'Custom_Tag_Name',
  75. },
  76. };
  77. const FITLER_KEY_SECTIONS: FilterKeySection[] = [
  78. {
  79. value: 'cat_1',
  80. label: 'Category 1',
  81. children: [
  82. FieldKey.ASSIGNED,
  83. FieldKey.BROWSER_NAME,
  84. FieldKey.IS,
  85. FieldKey.LAST_SEEN,
  86. FieldKey.TIMES_SEEN,
  87. ],
  88. },
  89. {
  90. value: 'cat_2',
  91. label: 'Category 2',
  92. children: [WebVital.LCP, MobileVital.FRAMES_SLOW_RATE],
  93. },
  94. {
  95. value: 'cat_3',
  96. label: 'Category 3',
  97. children: ['custom_tag_name'],
  98. },
  99. ];
  100. const getTagValues = (): Promise<string[]> => {
  101. return new Promise(resolve => {
  102. setTimeout(() => {
  103. resolve(['foo', 'bar', 'baz']);
  104. }, 500);
  105. });
  106. };
  107. export default storyBook(SearchQueryBuilder, story => {
  108. story('Getting started', () => {
  109. return (
  110. <Fragment>
  111. <p>
  112. <JSXNode name="SearchQueryBuilder" /> is a component which allows you to build a
  113. search query using a set of predefined filter keys and values.
  114. </p>
  115. <p>
  116. The search query, unless configured otherwise, may contain filters, logical
  117. operators, and free text. These filters can have defined data types, but default
  118. to a multi-selectable string filter.
  119. </p>
  120. <p>
  121. Required props:
  122. <ul>
  123. <li>
  124. <strong>
  125. <code>initialQuery</code>
  126. </strong>
  127. : The initial query to display in the search input.
  128. </li>
  129. <li>
  130. <strong>
  131. <code>filterKeys</code>
  132. </strong>
  133. : A collection of filter keys which are used to populate the dropdowns. All
  134. valid filter keys should be defined here.
  135. </li>
  136. <li>
  137. <strong>
  138. <code>getTagValues</code>
  139. </strong>
  140. : A function which returns an array of filter value suggestions. Any filter
  141. key which does not have <code>predefined: true</code> will use this function
  142. to get value suggestions.
  143. </li>
  144. <li>
  145. <strong>
  146. <code>searchSource</code>
  147. </strong>
  148. : Used to differentiate between different search bars for analytics.
  149. Typically snake_case (e.g. <code>issue_details</code>,{' '}
  150. <code>performance_landing</code>).
  151. </li>
  152. </ul>
  153. </p>
  154. <SearchQueryBuilder
  155. initialQuery="is:unresolved browser.name:[Firefox,Chrome] lastSeen:-7d timesSeen:>20 measurements.lcp:>300ms measurements.frames_slow_rate:<0.2"
  156. filterKeys={FILTER_KEYS}
  157. getTagValues={getTagValues}
  158. searchSource="storybook"
  159. />
  160. </Fragment>
  161. );
  162. });
  163. story('Defining filter value suggestions', () => {
  164. const filterValueSuggestionKeys: TagCollection = {
  165. predefined_values: {
  166. key: 'predefined_values',
  167. name: 'predefined_values',
  168. kind: FieldKind.FIELD,
  169. predefined: true,
  170. values: ['value1', 'value2', 'value3'],
  171. },
  172. predefined_categorized_values: {
  173. key: 'predefined_categorized_values',
  174. name: 'predefined_categorized_values',
  175. kind: FieldKind.FIELD,
  176. predefined: true,
  177. values: [
  178. {
  179. title: 'Category 1',
  180. type: 'header',
  181. icon: null,
  182. children: [{value: 'special value 1'}],
  183. },
  184. {
  185. title: 'Category 2',
  186. type: 'header',
  187. icon: null,
  188. children: [{value: 'special value 2'}, {value: 'special value 3'}],
  189. },
  190. ],
  191. },
  192. predefined_described_values: {
  193. key: 'predefined_described_values',
  194. name: 'predefined_described_values',
  195. kind: FieldKind.FIELD,
  196. predefined: true,
  197. values: [
  198. {
  199. title: '',
  200. type: ItemType.TAG_VALUE,
  201. value: 'special value 1',
  202. icon: null,
  203. documentation: 'Description for value 1',
  204. children: [],
  205. },
  206. {
  207. title: '',
  208. type: ItemType.TAG_VALUE,
  209. value: 'special value 2',
  210. icon: null,
  211. documentation: 'Description for value 2',
  212. children: [],
  213. },
  214. ],
  215. },
  216. async_values: {
  217. key: 'async_values',
  218. name: 'async_values',
  219. kind: FieldKind.FIELD,
  220. predefined: false,
  221. },
  222. };
  223. return (
  224. <Fragment>
  225. <p>
  226. To guide the user in building a search query, filter value suggestions can be
  227. provided in a few different ways:
  228. </p>
  229. <p>
  230. <ul>
  231. <li>
  232. <strong>Predefined</strong>: If the full set of filter keys are already
  233. known, they can be provided directly in <code>filterKeys</code>. These
  234. suggestions can also be formatted:
  235. <ul>
  236. <li>
  237. <strong>Simple</strong>: For most cases, an array of strings can be
  238. provided in <code>values</code>.
  239. </li>
  240. <li>
  241. <strong>Categorized</strong>: If the values should be grouped, an array
  242. of objects can be provided in <code>values</code>. Each object should
  243. have a <code>title</code> and <code>children</code> array.
  244. </li>
  245. <li>
  246. <strong>Described</strong>: If descriptions are necessary, provide an
  247. array of objects of type <code>ItemType.TAG_VALUE</code> with a{' '}
  248. <code>documentation</code> property.
  249. </li>
  250. </ul>
  251. </li>
  252. <li>
  253. <strong>Aync</strong>: If the filter key does not have{' '}
  254. <code>predefined: true</code>, it will use the <code>getTagValues</code>{' '}
  255. function to fetch suggestions. The filter key and query are provided, and it
  256. is up to the consumer to return the suggestions.
  257. </li>
  258. </ul>
  259. </p>
  260. <SearchQueryBuilder
  261. initialQuery=""
  262. filterKeys={filterValueSuggestionKeys}
  263. getTagValues={getTagValues}
  264. searchSource="storybook"
  265. />
  266. </Fragment>
  267. );
  268. });
  269. story('Customizing the filter key menu', () => {
  270. return (
  271. <Fragment>
  272. <p>
  273. A special menu can be displayed when no text is entered in the search input,
  274. allowing for better oranization and discovery of filter keys.
  275. </p>
  276. <p>
  277. This menu is defined by <code>filterKeySections</code>, which accepts a list of
  278. sections. Each section contains a name and a list of filter keys. Note that the
  279. order of both the sections and the items within each section are respected.
  280. </p>
  281. <SearchQueryBuilder
  282. initialQuery=""
  283. filterKeySections={FITLER_KEY_SECTIONS}
  284. filterKeys={FILTER_KEYS}
  285. getTagValues={getTagValues}
  286. searchSource="storybook"
  287. />
  288. </Fragment>
  289. );
  290. });
  291. story('Field definitions', () => {
  292. return (
  293. <Fragment>
  294. <p>
  295. Field definitions very important for the search query builder to work correctly.
  296. They provide information such as what data types are allow for a given filter,
  297. as well as the description and keywords.
  298. </p>
  299. <p>
  300. By default, field definitions are sourced from{' '}
  301. <code>EVENT_FIELD_DEFINITIONS</code> in <code>sentry/utils/fields.ts</code>. If
  302. these definitions are not correct for the use case, they can be overridden by
  303. passing <code>fieldDefinitionGetter</code>.
  304. </p>
  305. <SearchQueryBuilder
  306. initialQuery=""
  307. filterKeys={{boolean_key: {key: 'boolean_key', name: 'boolean_key'}}}
  308. getTagValues={getTagValues}
  309. fieldDefinitionGetter={() => {
  310. return {
  311. desc: 'Customized field defintion',
  312. kind: FieldKind.FIELD,
  313. valueType: FieldValueType.BOOLEAN,
  314. };
  315. }}
  316. searchSource="storybook"
  317. />
  318. </Fragment>
  319. );
  320. });
  321. story('Callbacks', () => {
  322. const [onChangeValue, setOnChangeValue] = useState<string>('');
  323. const [onSearchValue, setOnSearchValue] = useState<string>('');
  324. return (
  325. <Fragment>
  326. <p>
  327. <code>onChange</code> is called whenever the search query changes. This can be
  328. used to update the UI as the user updates the query.
  329. </p>
  330. <p>
  331. <code>onSearch</code> is called when the user presses enter. This can be used to
  332. submit the search query.
  333. </p>
  334. <p>
  335. <ul>
  336. <li>
  337. <strong>
  338. Last <code>onChange</code> value
  339. </strong>
  340. : <code>{onChangeValue}</code>
  341. </li>
  342. <li>
  343. <strong>
  344. Last <code>onSearch</code> value
  345. </strong>
  346. : <code>{onSearchValue}</code>
  347. </li>
  348. </ul>
  349. </p>
  350. <SearchQueryBuilder
  351. initialQuery=""
  352. filterKeySections={FITLER_KEY_SECTIONS}
  353. filterKeys={FILTER_KEYS}
  354. getTagValues={getTagValues}
  355. searchSource="storybook"
  356. onChange={setOnChangeValue}
  357. onSearch={setOnSearchValue}
  358. />
  359. </Fragment>
  360. );
  361. });
  362. story('Configuring valid syntax', () => {
  363. const configs = [
  364. 'disallowFreeText',
  365. 'disallowLogicalOperators',
  366. 'disallowWildcard',
  367. 'disallowUnsupportedFilters',
  368. ];
  369. const [enabledConfigs, setEnabledConfigs] = useState<string[]>([...configs]);
  370. const queryBuilderOptions = enabledConfigs.reduce((acc, config) => {
  371. acc[config] = true;
  372. return acc;
  373. }, {});
  374. return (
  375. <Fragment>
  376. <p>
  377. There are some config options which allow you to customize which types of syntax
  378. are considered valid. This should be used when the search backend does not
  379. support certain operators like boolean logic or wildcards. Use the checkboxes
  380. below to enable/disable the following options:
  381. </p>
  382. <MultipleCheckbox
  383. onChange={setEnabledConfigs}
  384. value={enabledConfigs}
  385. name="enabled configs"
  386. >
  387. {configs.map(config => (
  388. <MultipleCheckbox.Item key={config} value={config}>
  389. <code>{config}</code>
  390. </MultipleCheckbox.Item>
  391. ))}
  392. </MultipleCheckbox>
  393. <SearchQueryBuilder
  394. initialQuery="(unsupported_key:value OR browser.name:Internet*) TypeError"
  395. filterKeySections={FITLER_KEY_SECTIONS}
  396. filterKeys={FILTER_KEYS}
  397. getTagValues={getTagValues}
  398. searchSource="storybook"
  399. {...queryBuilderOptions}
  400. />
  401. <p>
  402. The query above has a few invalid tokens. The invalid tokens are highlighted in
  403. red and display a tooltip with a message when focused. The invalid token
  404. messages can be customized using the <code>invalidMessages</code> prop. In this
  405. case, the unsupported tag message is modified with{' '}
  406. <JSXProperty
  407. name="invalidMessages"
  408. value={{[InvalidReason.LOGICAL_AND_NOT_ALLOWED]: 'foo bar baz'}}
  409. />
  410. .
  411. </p>
  412. <SearchQueryBuilder
  413. initialQuery="AND"
  414. filterKeySections={FITLER_KEY_SECTIONS}
  415. filterKeys={FILTER_KEYS}
  416. getTagValues={getTagValues}
  417. searchSource="storybook"
  418. disallowLogicalOperators
  419. invalidMessages={{[InvalidReason.LOGICAL_AND_NOT_ALLOWED]: 'foo bar baz'}}
  420. />
  421. </Fragment>
  422. );
  423. });
  424. story('Disabled', () => {
  425. return (
  426. <SearchQueryBuilder
  427. initialQuery="is:unresolved assigned:me"
  428. filterKeys={FILTER_KEYS}
  429. getTagValues={getTagValues}
  430. searchSource="storybook"
  431. disabled
  432. />
  433. );
  434. });
  435. story('Migrating from SmartSearchBar', () => {
  436. return (
  437. <Fragment>
  438. <p>
  439. <JSXNode name="SearchQueryBuilder" /> is a replacement for{' '}
  440. <JSXNode name="SmartSearchBar" />. It provides a more flexible and powerful
  441. search query builder.
  442. </p>
  443. <p>
  444. Some props have been renamed:
  445. <ul>
  446. <li>
  447. <code>supportedTags</code> {'->'} <code>filterKeys</code>
  448. </li>
  449. <li>
  450. <code>onGetTagValues</code> {'->'} <code>getTagValues</code>
  451. </li>
  452. <li>
  453. <code>highlightUnsupportedTags</code> {'->'}{' '}
  454. <code>disallowUnsupportedFilters</code>
  455. </li>
  456. <li>
  457. <code>savedSearchType</code> {'->'} <code>recentSearches</code>
  458. </li>
  459. </ul>
  460. </p>
  461. <p>
  462. Some props have been removed:
  463. <ul>
  464. <li>
  465. <code>excludedTags</code> is no longer supported. If a filter key should not
  466. be shown, do not include it in <code>filterKeys</code>.
  467. </li>
  468. <li>
  469. <code>(boolean|date|duration)Keys</code> no longer need to be specified. The
  470. filter value types are inferred from the field definitions.
  471. </li>
  472. <li>
  473. <code>projectIds</code> was used to add <code>is_multi_project</code> to
  474. some of the analytics events. If your use case requires this, you can record
  475. these events manually with the <code>onSearch</code> callback.
  476. </li>
  477. <li>
  478. <code>hasRecentSearches</code> is no longer required. Saved searches will be
  479. saved and displayed when <code>recentSearches</code> is provided.
  480. </li>
  481. </ul>
  482. </p>
  483. </Fragment>
  484. );
  485. });
  486. });