index.stories.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. import {Fragment, useState} from 'react';
  2. import MultipleCheckbox from 'sentry/components/forms/controls/multipleCheckbox';
  3. import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';
  4. import {FormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery';
  5. import type {
  6. FieldDefinitionGetter,
  7. FilterKeySection,
  8. } from 'sentry/components/searchQueryBuilder/types';
  9. import {InvalidReason} from 'sentry/components/searchSyntax/parser';
  10. import {ItemType} from 'sentry/components/smartSearchBar/types';
  11. import JSXNode from 'sentry/components/stories/jsxNode';
  12. import JSXProperty from 'sentry/components/stories/jsxProperty';
  13. import storyBook from 'sentry/stories/storyBook';
  14. import type {TagCollection} from 'sentry/types/group';
  15. import {
  16. FieldKey,
  17. FieldKind,
  18. FieldValueType,
  19. getFieldDefinition,
  20. MobileVital,
  21. WebVital,
  22. } from 'sentry/utils/fields';
  23. const FILTER_KEYS: TagCollection = {
  24. [FieldKey.ASSIGNED]: {
  25. key: FieldKey.ASSIGNED,
  26. name: 'Assigned To',
  27. kind: FieldKind.FIELD,
  28. predefined: true,
  29. values: [
  30. {
  31. title: 'Suggested',
  32. type: 'header',
  33. icon: null,
  34. children: [{value: 'me'}, {value: 'unassigned'}],
  35. },
  36. {
  37. title: 'All',
  38. type: 'header',
  39. icon: null,
  40. children: [{value: 'person1@sentry.io'}, {value: 'person2@sentry.io'}],
  41. },
  42. ],
  43. },
  44. [FieldKey.BROWSER_NAME]: {
  45. key: FieldKey.BROWSER_NAME,
  46. name: 'Browser Name',
  47. kind: FieldKind.FIELD,
  48. predefined: true,
  49. values: ['Chrome', 'Firefox', 'Safari', 'Edge', 'Internet Explorer', 'Opera 1,2'],
  50. },
  51. [FieldKey.IS]: {
  52. key: FieldKey.IS,
  53. name: 'is',
  54. predefined: true,
  55. values: ['resolved', 'unresolved', 'ignored'],
  56. },
  57. [FieldKey.LAST_SEEN]: {
  58. key: FieldKey.LAST_SEEN,
  59. name: 'lastSeen',
  60. kind: FieldKind.FIELD,
  61. },
  62. [FieldKey.TIMES_SEEN]: {
  63. key: FieldKey.TIMES_SEEN,
  64. name: 'timesSeen',
  65. kind: FieldKind.FIELD,
  66. },
  67. [WebVital.LCP]: {
  68. key: WebVital.LCP,
  69. name: 'lcp',
  70. kind: FieldKind.FIELD,
  71. },
  72. [MobileVital.FRAMES_SLOW_RATE]: {
  73. key: MobileVital.FRAMES_SLOW_RATE,
  74. name: 'framesSlowRate',
  75. kind: FieldKind.FIELD,
  76. },
  77. custom_tag_name: {
  78. key: 'custom_tag_name',
  79. name: 'Custom_Tag_Name',
  80. },
  81. };
  82. const FILTER_KEY_SECTIONS: FilterKeySection[] = [
  83. {
  84. value: 'cat_1',
  85. label: 'Category 1',
  86. children: [FieldKey.ASSIGNED, FieldKey.IS],
  87. },
  88. {
  89. value: 'cat_2',
  90. label: 'Category 2',
  91. children: [WebVital.LCP, MobileVital.FRAMES_SLOW_RATE],
  92. },
  93. {
  94. value: 'cat_3',
  95. label: 'Category 3',
  96. children: [FieldKey.TIMES_SEEN],
  97. },
  98. {
  99. value: 'cat_4',
  100. label: 'Category 4',
  101. children: [FieldKey.LAST_SEEN, FieldKey.TIMES_SEEN],
  102. },
  103. {
  104. value: 'cat_5',
  105. label: 'Category 5',
  106. children: ['custom_tag_name'],
  107. },
  108. ];
  109. const getTagValues = (): Promise<string[]> => {
  110. return new Promise(resolve => {
  111. setTimeout(() => {
  112. resolve(['foo', 'bar', 'baz']);
  113. }, 500);
  114. });
  115. };
  116. export default storyBook(SearchQueryBuilder, story => {
  117. story('Getting started', () => {
  118. return (
  119. <Fragment>
  120. <p>
  121. <JSXNode name="SearchQueryBuilder" /> is a component which allows you to build a
  122. search query using a set of predefined filter keys and values.
  123. </p>
  124. <p>
  125. The search query, unless configured otherwise, may contain filters, logical
  126. operators, and free text. These filters can have defined data types, but default
  127. to a multi-selectable string filter.
  128. </p>
  129. <p>
  130. Required props:
  131. <ul>
  132. <li>
  133. <strong>
  134. <code>initialQuery</code>
  135. </strong>
  136. : The initial query to display in the search input.
  137. </li>
  138. <li>
  139. <strong>
  140. <code>filterKeys</code>
  141. </strong>
  142. : A collection of filter keys which are used to populate the dropdowns. All
  143. valid filter keys should be defined here.
  144. </li>
  145. <li>
  146. <strong>
  147. <code>getTagValues</code>
  148. </strong>
  149. : A function which returns an array of filter value suggestions. Any filter
  150. key which does not have <code>predefined: true</code> will use this function
  151. to get value suggestions.
  152. </li>
  153. <li>
  154. <strong>
  155. <code>searchSource</code>
  156. </strong>
  157. : Used to differentiate between different search bars for analytics.
  158. Typically snake_case (e.g. <code>issue_details</code>,{' '}
  159. <code>performance_landing</code>).
  160. </li>
  161. </ul>
  162. </p>
  163. <SearchQueryBuilder
  164. initialQuery="is:unresolved browser.name:[Firefox,Chrome] lastSeen:-7d timesSeen:>20 measurements.lcp:>300ms measurements.frames_slow_rate:<0.2"
  165. filterKeys={FILTER_KEYS}
  166. getTagValues={getTagValues}
  167. searchSource="storybook"
  168. />
  169. </Fragment>
  170. );
  171. });
  172. story('Defining filter value suggestions', () => {
  173. const filterValueSuggestionKeys: TagCollection = {
  174. predefined_values: {
  175. key: 'predefined_values',
  176. name: 'predefined_values',
  177. kind: FieldKind.FIELD,
  178. predefined: true,
  179. values: ['value1', 'value2', 'value3'],
  180. },
  181. predefined_categorized_values: {
  182. key: 'predefined_categorized_values',
  183. name: 'predefined_categorized_values',
  184. kind: FieldKind.FIELD,
  185. predefined: true,
  186. values: [
  187. {
  188. title: 'Category 1',
  189. type: 'header',
  190. icon: null,
  191. children: [{value: 'special value 1'}],
  192. },
  193. {
  194. title: 'Category 2',
  195. type: 'header',
  196. icon: null,
  197. children: [{value: 'special value 2'}, {value: 'special value 3'}],
  198. },
  199. ],
  200. },
  201. predefined_described_values: {
  202. key: 'predefined_described_values',
  203. name: 'predefined_described_values',
  204. kind: FieldKind.FIELD,
  205. predefined: true,
  206. values: [
  207. {
  208. title: '',
  209. type: ItemType.TAG_VALUE,
  210. value: 'special value 1',
  211. icon: null,
  212. documentation: 'Description for value 1',
  213. children: [],
  214. },
  215. {
  216. title: '',
  217. type: ItemType.TAG_VALUE,
  218. value: 'special value 2',
  219. icon: null,
  220. documentation: 'Description for value 2',
  221. children: [],
  222. },
  223. ],
  224. },
  225. async_values: {
  226. key: 'async_values',
  227. name: 'async_values',
  228. kind: FieldKind.FIELD,
  229. predefined: false,
  230. },
  231. };
  232. return (
  233. <Fragment>
  234. <p>
  235. To guide the user in building a search query, filter value suggestions can be
  236. provided in a few different ways:
  237. </p>
  238. <p>
  239. <ul>
  240. <li>
  241. <strong>Predefined</strong>: If the full set of filter keys are already
  242. known, they can be provided directly in <code>filterKeys</code>. These
  243. suggestions can also be formatted:
  244. <ul>
  245. <li>
  246. <strong>Simple</strong>: For most cases, an array of strings can be
  247. provided in <code>values</code>.
  248. </li>
  249. <li>
  250. <strong>Categorized</strong>: If the values should be grouped, an array
  251. of objects can be provided in <code>values</code>. Each object should
  252. have a <code>title</code> and <code>children</code> array.
  253. </li>
  254. <li>
  255. <strong>Described</strong>: If descriptions are necessary, provide an
  256. array of objects of type <code>ItemType.TAG_VALUE</code> with a{' '}
  257. <code>documentation</code> property.
  258. </li>
  259. </ul>
  260. </li>
  261. <li>
  262. <strong>Async</strong>: If the filter key does not have{' '}
  263. <code>predefined: true</code>, it will use the <code>getTagValues</code>{' '}
  264. function to fetch suggestions. The filter key and query are provided, and it
  265. is up to the consumer to return the suggestions.
  266. </li>
  267. </ul>
  268. </p>
  269. <SearchQueryBuilder
  270. initialQuery=""
  271. filterKeys={filterValueSuggestionKeys}
  272. getTagValues={getTagValues}
  273. searchSource="storybook"
  274. />
  275. </Fragment>
  276. );
  277. });
  278. story('Customizing the filter key menu', () => {
  279. return (
  280. <Fragment>
  281. <p>
  282. A special menu can be displayed when no text is entered in the search input,
  283. allowing for better organization and discovery of filter keys.
  284. </p>
  285. <p>
  286. This menu is defined by <code>filterKeySections</code>, which accepts a list of
  287. sections. Each section contains a name and a list of filter keys. Note that the
  288. order of both the sections and the items within each section are respected.
  289. </p>
  290. <SearchQueryBuilder
  291. initialQuery=""
  292. filterKeySections={FILTER_KEY_SECTIONS}
  293. filterKeys={FILTER_KEYS}
  294. getTagValues={getTagValues}
  295. searchSource="storybook"
  296. />
  297. <p>
  298. If you wish to modify the size of the filter key menu, use
  299. <code>filterKeyMenuWidth</code> to define the width in pixels.
  300. </p>
  301. <SearchQueryBuilder
  302. initialQuery=""
  303. filterKeySections={FILTER_KEY_SECTIONS}
  304. filterKeys={FILTER_KEYS}
  305. getTagValues={getTagValues}
  306. searchSource="storybook"
  307. filterKeyMenuWidth={600}
  308. />
  309. </Fragment>
  310. );
  311. });
  312. story('Field definitions', () => {
  313. return (
  314. <Fragment>
  315. <p>
  316. Field definitions very important for the search query builder to work correctly.
  317. They provide information such as what data types are allow for a given filter,
  318. as well as the description and keywords.
  319. </p>
  320. <p>
  321. By default, field definitions are sourced from{' '}
  322. <code>EVENT_FIELD_DEFINITIONS</code> in <code>sentry/utils/fields.ts</code>. If
  323. these definitions are not correct for the use case, they can be overridden by
  324. passing <code>fieldDefinitionGetter</code>.
  325. </p>
  326. <SearchQueryBuilder
  327. initialQuery=""
  328. filterKeys={{boolean_key: {key: 'boolean_key', name: 'boolean_key'}}}
  329. getTagValues={getTagValues}
  330. fieldDefinitionGetter={() => {
  331. return {
  332. desc: 'Customized field definition',
  333. kind: FieldKind.FIELD,
  334. valueType: FieldValueType.BOOLEAN,
  335. };
  336. }}
  337. searchSource="storybook"
  338. />
  339. </Fragment>
  340. );
  341. });
  342. story('Aggregate filters', () => {
  343. const aggregateFilterKeys: TagCollection = {
  344. apdex: {
  345. key: 'apdex',
  346. name: 'apdex',
  347. kind: FieldKind.FUNCTION,
  348. },
  349. count: {
  350. key: 'count',
  351. name: 'count',
  352. kind: FieldKind.FUNCTION,
  353. },
  354. count_if: {
  355. key: 'count_if',
  356. name: 'count_if',
  357. kind: FieldKind.FUNCTION,
  358. },
  359. p95: {
  360. key: 'p95',
  361. name: 'p95',
  362. kind: FieldKind.FUNCTION,
  363. },
  364. 'transaction.duration': {
  365. key: 'transaction.duration',
  366. name: 'transaction.duration',
  367. kind: FieldKind.FIELD,
  368. },
  369. timesSeen: {
  370. key: 'timesSeen',
  371. name: 'timesSeen',
  372. kind: FieldKind.FIELD,
  373. },
  374. lastSeen: {
  375. key: 'lastSeen',
  376. name: 'lastSeen',
  377. kind: FieldKind.FIELD,
  378. },
  379. };
  380. const getAggregateFieldDefinition: FieldDefinitionGetter = (key: string) => {
  381. switch (key) {
  382. case 'apdex':
  383. return {
  384. desc: 'Returns results with the Apdex score that you entered. Values must be between 0 and 1. Higher apdex values indicate higher user satisfaction.',
  385. kind: FieldKind.FUNCTION,
  386. valueType: FieldValueType.NUMBER,
  387. parameters: [
  388. {
  389. name: 'threshold',
  390. kind: 'value' as const,
  391. dataType: FieldValueType.NUMBER,
  392. defaultValue: '300',
  393. required: true,
  394. },
  395. ],
  396. };
  397. case 'count':
  398. return {
  399. desc: 'Returns results with a matching count.',
  400. kind: FieldKind.FUNCTION,
  401. valueType: FieldValueType.INTEGER,
  402. parameters: [],
  403. };
  404. case 'count_if':
  405. return {
  406. desc: 'Returns results with a matching count that satisfy the condition passed to the parameters of the function.',
  407. kind: FieldKind.FUNCTION,
  408. valueType: FieldValueType.INTEGER,
  409. parameters: [
  410. {
  411. name: 'column',
  412. kind: 'column' as const,
  413. columnTypes: [
  414. FieldValueType.STRING,
  415. FieldValueType.NUMBER,
  416. FieldValueType.DURATION,
  417. ],
  418. defaultValue: 'transaction.duration',
  419. required: true,
  420. },
  421. {
  422. name: 'operator',
  423. kind: 'value' as const,
  424. options: [
  425. {
  426. label: 'is equal to',
  427. value: 'equals',
  428. },
  429. {
  430. label: 'is not equal to',
  431. value: 'notEquals',
  432. },
  433. {
  434. label: 'is less than',
  435. value: 'less',
  436. },
  437. {
  438. label: 'is greater than',
  439. value: 'greater',
  440. },
  441. {
  442. label: 'is less than or equal to',
  443. value: 'lessOrEquals',
  444. },
  445. {
  446. label: 'is greater than or equal to',
  447. value: 'greaterOrEquals',
  448. },
  449. ],
  450. dataType: FieldValueType.STRING,
  451. defaultValue: 'equals',
  452. required: true,
  453. },
  454. {
  455. name: 'value',
  456. kind: 'value',
  457. dataType: FieldValueType.STRING,
  458. defaultValue: '300ms',
  459. required: true,
  460. },
  461. ],
  462. };
  463. case 'p95':
  464. return {
  465. desc: 'Returns results with the 95th percentile of the selected column.',
  466. kind: FieldKind.FUNCTION,
  467. defaultValue: '300ms',
  468. valueType: null,
  469. parameterDependentValueType: parameters => {
  470. const column = parameters[0];
  471. const fieldDef = column ? getFieldDefinition(column) : null;
  472. return fieldDef?.valueType ?? FieldValueType.NUMBER;
  473. },
  474. parameters: [
  475. {
  476. name: 'column',
  477. kind: 'column' as const,
  478. columnTypes: [
  479. FieldValueType.DURATION,
  480. FieldValueType.NUMBER,
  481. FieldValueType.INTEGER,
  482. FieldValueType.PERCENTAGE,
  483. ],
  484. defaultValue: 'transaction.duration',
  485. required: true,
  486. },
  487. ],
  488. };
  489. default:
  490. return getFieldDefinition(key);
  491. }
  492. };
  493. return (
  494. <Fragment>
  495. <p>
  496. Filter keys can be defined as aggregate filters, which allow for more complex
  497. operations. They may accept any number of parameters, which are defined in the
  498. field definition.
  499. </p>
  500. <p>
  501. To define an aggregate filter, set the <code>kind</code> to{' '}
  502. <code>FieldKind.FUNCTION</code>, and the <code>valueType</code> to the return
  503. type of the function. Then define the <code>parameters</code>, which is an array
  504. of acceptable column types or a predicate function.
  505. </p>
  506. <ul>
  507. <li>
  508. <strong>
  509. <code>name</code>
  510. </strong>
  511. : The name of the parameter.
  512. <li>
  513. <strong>
  514. <code>kind</code>
  515. </strong>
  516. : Parameters may be defined as either a column parameter or a value
  517. parameter.
  518. <ul>
  519. <li>
  520. <code>'value'</code>: If this parameter is a value it also requires a{' '}
  521. <code>dataType</code> and, optionally, a list of <code>options</code>{' '}
  522. that will be displayed as suggestions.
  523. </li>
  524. <li>
  525. <code>'column'</code>: Column parameters suggest other existing filter
  526. keys. This also requires <code>columnTypes</code> to be defined, which
  527. may be a list of data types that the column may be or a predicate
  528. function.
  529. </li>
  530. </ul>
  531. </li>
  532. <li>
  533. <strong>
  534. <code>required</code>
  535. </strong>
  536. : Whether or not the parameter is required.
  537. </li>
  538. <li>
  539. <strong>
  540. <code>defaultValue</code>
  541. </strong>
  542. : The default value that the parameter will be set to when the filter is
  543. first added.
  544. </li>
  545. </li>
  546. </ul>
  547. <p>
  548. Some aggreate filters may have a return type that is dependent on the
  549. parameters. For example, <code>p95(column)</code> may return a few different
  550. types depending on the column type. In this case, the field definition should
  551. implement <code>parameterDependentValueType</code>. This function accepts an
  552. array of parameters and returns the value type.
  553. </p>
  554. <SearchQueryBuilder
  555. initialQuery=""
  556. filterKeys={aggregateFilterKeys}
  557. getTagValues={getTagValues}
  558. fieldDefinitionGetter={getAggregateFieldDefinition}
  559. searchSource="storybook"
  560. />
  561. </Fragment>
  562. );
  563. });
  564. story('Callbacks', () => {
  565. const [onChangeValue, setOnChangeValue] = useState<string>('');
  566. const [onSearchValue, setOnSearchValue] = useState<string>('');
  567. return (
  568. <Fragment>
  569. <p>
  570. <code>onChange</code> is called whenever the search query changes. This can be
  571. used to update the UI as the user updates the query.
  572. </p>
  573. <p>
  574. <code>onSearch</code> is called when the user presses enter. This can be used to
  575. submit the search query.
  576. </p>
  577. <p>
  578. <ul>
  579. <li>
  580. <strong>
  581. Last <code>onChange</code> value
  582. </strong>
  583. : <code>{onChangeValue}</code>
  584. </li>
  585. <li>
  586. <strong>
  587. Last <code>onSearch</code> value
  588. </strong>
  589. : <code>{onSearchValue}</code>
  590. </li>
  591. </ul>
  592. </p>
  593. <SearchQueryBuilder
  594. initialQuery=""
  595. filterKeySections={FILTER_KEY_SECTIONS}
  596. filterKeys={FILTER_KEYS}
  597. getTagValues={getTagValues}
  598. searchSource="storybook"
  599. onChange={setOnChangeValue}
  600. onSearch={setOnSearchValue}
  601. />
  602. </Fragment>
  603. );
  604. });
  605. story('Configuring valid syntax', () => {
  606. const configs = [
  607. 'disallowFreeText',
  608. 'disallowLogicalOperators',
  609. 'disallowWildcard',
  610. 'disallowUnsupportedFilters',
  611. ];
  612. const [enabledConfigs, setEnabledConfigs] = useState<string[]>([...configs]);
  613. const queryBuilderOptions = enabledConfigs.reduce((acc, config) => {
  614. acc[config] = true;
  615. return acc;
  616. }, {});
  617. return (
  618. <Fragment>
  619. <p>
  620. There are some config options which allow you to customize which types of syntax
  621. are considered valid. This should be used when the search backend does not
  622. support certain operators like boolean logic or wildcards. Use the checkboxes
  623. below to enable/disable the following options:
  624. </p>
  625. <MultipleCheckbox
  626. onChange={setEnabledConfigs}
  627. value={enabledConfigs}
  628. name="enabled configs"
  629. >
  630. {configs.map(config => (
  631. <MultipleCheckbox.Item key={config} value={config}>
  632. <code>{config}</code>
  633. </MultipleCheckbox.Item>
  634. ))}
  635. </MultipleCheckbox>
  636. <SearchQueryBuilder
  637. initialQuery="(unsupported_key:value OR browser.name:Internet*) TypeError"
  638. filterKeySections={FILTER_KEY_SECTIONS}
  639. filterKeys={FILTER_KEYS}
  640. getTagValues={getTagValues}
  641. searchSource="storybook"
  642. {...queryBuilderOptions}
  643. />
  644. <p>
  645. The query above has a few invalid tokens. The invalid tokens are highlighted in
  646. red and display a tooltip with a message when focused. The invalid token
  647. messages can be customized using the <code>invalidMessages</code> prop. In this
  648. case, the unsupported tag message is modified with{' '}
  649. <JSXProperty
  650. name="invalidMessages"
  651. value={{[InvalidReason.LOGICAL_AND_NOT_ALLOWED]: 'foo bar baz'}}
  652. />
  653. .
  654. </p>
  655. <SearchQueryBuilder
  656. initialQuery="AND"
  657. filterKeySections={FILTER_KEY_SECTIONS}
  658. filterKeys={FILTER_KEYS}
  659. getTagValues={getTagValues}
  660. searchSource="storybook"
  661. disallowLogicalOperators
  662. invalidMessages={{[InvalidReason.LOGICAL_AND_NOT_ALLOWED]: 'foo bar baz'}}
  663. />
  664. </Fragment>
  665. );
  666. });
  667. story('Unsubmitted search indicator', () => {
  668. const [query, setQuery] = useState('is:unresolved assigned:me');
  669. return (
  670. <Fragment>
  671. <p>
  672. You can display an indicator when the search query has been modified but not
  673. fully submitted using the <code>showUnsubmittedIndicator</code> prop. This can
  674. be useful to remind the user that they have unsaved changes for use cases which
  675. require manual submission.
  676. </p>
  677. <p>
  678. Current query: <code>{query}</code>
  679. </p>
  680. <SearchQueryBuilder
  681. initialQuery={query}
  682. filterKeys={FILTER_KEYS}
  683. getTagValues={getTagValues}
  684. searchSource="storybook"
  685. showUnsubmittedIndicator
  686. onSearch={setQuery}
  687. />
  688. </Fragment>
  689. );
  690. });
  691. story('Disabled', () => {
  692. return (
  693. <SearchQueryBuilder
  694. initialQuery="is:unresolved assigned:me"
  695. filterKeys={FILTER_KEYS}
  696. getTagValues={getTagValues}
  697. searchSource="storybook"
  698. disabled
  699. />
  700. );
  701. });
  702. story('FormattedQuery', () => {
  703. return (
  704. <Fragment>
  705. <p>
  706. If you just need to render a formatted query outside of the search bar,{' '}
  707. <JSXNode name="FormattedQuery" /> is exported for this purpose:
  708. </p>
  709. <FormattedQuery
  710. query="count():>1 AND (browser.name:[Firefox,Chrome] OR lastSeen:-7d) TypeError"
  711. filterKeys={FILTER_KEYS}
  712. />
  713. </Fragment>
  714. );
  715. });
  716. story('Migrating from SmartSearchBar', () => {
  717. return (
  718. <Fragment>
  719. <p>
  720. <JSXNode name="SearchQueryBuilder" /> is a replacement for{' '}
  721. <JSXNode name="SmartSearchBar" />. It provides a more flexible and powerful
  722. search query builder.
  723. </p>
  724. <p>
  725. Some props have been renamed:
  726. <ul>
  727. <li>
  728. <code>supportedTags</code> {'->'} <code>filterKeys</code>
  729. </li>
  730. <li>
  731. <code>onGetTagValues</code> {'->'} <code>getTagValues</code>
  732. </li>
  733. <li>
  734. <code>highlightUnsupportedTags</code> {'->'}{' '}
  735. <code>disallowUnsupportedFilters</code>
  736. </li>
  737. <li>
  738. <code>savedSearchType</code> {'->'} <code>recentSearches</code>
  739. </li>
  740. </ul>
  741. </p>
  742. <p>
  743. Some props have been removed:
  744. <ul>
  745. <li>
  746. <code>excludedTags</code> is no longer supported. If a filter key should not
  747. be shown, do not include it in <code>filterKeys</code>.
  748. </li>
  749. <li>
  750. <code>(boolean|date|duration)Keys</code> no longer need to be specified. The
  751. filter value types are inferred from the field definitions.
  752. </li>
  753. <li>
  754. <code>projectIds</code> was used to add <code>is_multi_project</code> to
  755. some of the analytics events. If your use case requires this, you can record
  756. these events manually with the <code>onSearch</code> callback.
  757. </li>
  758. <li>
  759. <code>hasRecentSearches</code> is no longer required. Saved searches will be
  760. saved and displayed when <code>recentSearches</code> is provided.
  761. </li>
  762. </ul>
  763. </p>
  764. </Fragment>
  765. );
  766. });
  767. });