import {Fragment, useState} from 'react'; import MultipleCheckbox from 'sentry/components/forms/controls/multipleCheckbox'; import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; import type {FilterKeySection} from 'sentry/components/searchQueryBuilder/types'; import {InvalidReason} from 'sentry/components/searchSyntax/parser'; import {ItemType} from 'sentry/components/smartSearchBar/types'; import JSXNode from 'sentry/components/stories/jsxNode'; import JSXProperty from 'sentry/components/stories/jsxProperty'; import storyBook from 'sentry/stories/storyBook'; import type {TagCollection} from 'sentry/types/group'; import { FieldKey, FieldKind, FieldValueType, MobileVital, WebVital, } from 'sentry/utils/fields'; const FILTER_KEYS: TagCollection = { [FieldKey.ASSIGNED]: { key: FieldKey.ASSIGNED, name: 'Assigned To', kind: FieldKind.FIELD, predefined: true, values: [ { title: 'Suggested', type: 'header', icon: null, children: [{value: 'me'}, {value: 'unassigned'}], }, { title: 'All', type: 'header', icon: null, children: [{value: 'person1@sentry.io'}, {value: 'person2@sentry.io'}], }, ], }, [FieldKey.BROWSER_NAME]: { key: FieldKey.BROWSER_NAME, name: 'Browser Name', kind: FieldKind.FIELD, predefined: true, values: ['Chrome', 'Firefox', 'Safari', 'Edge'], }, [FieldKey.IS]: { key: FieldKey.IS, name: 'is', predefined: true, values: ['resolved', 'unresolved', 'ignored'], }, [FieldKey.LAST_SEEN]: { key: FieldKey.LAST_SEEN, name: 'lastSeen', kind: FieldKind.FIELD, }, [FieldKey.TIMES_SEEN]: { key: FieldKey.TIMES_SEEN, name: 'timesSeen', kind: FieldKind.FIELD, }, [WebVital.LCP]: { key: WebVital.LCP, name: 'lcp', kind: FieldKind.FIELD, }, [MobileVital.FRAMES_SLOW_RATE]: { key: MobileVital.FRAMES_SLOW_RATE, name: 'framesSlowRate', kind: FieldKind.FIELD, }, custom_tag_name: { key: 'custom_tag_name', name: 'Custom_Tag_Name', }, }; const FITLER_KEY_SECTIONS: FilterKeySection[] = [ { value: 'cat_1', label: 'Category 1', children: [ FieldKey.ASSIGNED, FieldKey.BROWSER_NAME, FieldKey.IS, FieldKey.LAST_SEEN, FieldKey.TIMES_SEEN, ], }, { value: 'cat_2', label: 'Category 2', children: [WebVital.LCP, MobileVital.FRAMES_SLOW_RATE], }, { value: 'cat_3', label: 'Category 3', children: ['custom_tag_name'], }, ]; const getTagValues = (): Promise => { return new Promise(resolve => { setTimeout(() => { resolve(['foo', 'bar', 'baz']); }, 500); }); }; export default storyBook(SearchQueryBuilder, story => { story('Getting started', () => { return (

is a component which allows you to build a search query using a set of predefined filter keys and values.

The search query, unless configured otherwise, may contain filters, logical operators, and free text. These filters can have defined data types, but default to a multi-selectable string filter.

Required props:

); }); story('Defining filter value suggestions', () => { const filterValueSuggestionKeys: TagCollection = { predefined_values: { key: 'predefined_values', name: 'predefined_values', kind: FieldKind.FIELD, predefined: true, values: ['value1', 'value2', 'value3'], }, predefined_categorized_values: { key: 'predefined_categorized_values', name: 'predefined_categorized_values', kind: FieldKind.FIELD, predefined: true, values: [ { title: 'Category 1', type: 'header', icon: null, children: [{value: 'special value 1'}], }, { title: 'Category 2', type: 'header', icon: null, children: [{value: 'special value 2'}, {value: 'special value 3'}], }, ], }, predefined_described_values: { key: 'predefined_described_values', name: 'predefined_described_values', kind: FieldKind.FIELD, predefined: true, values: [ { title: '', type: ItemType.TAG_VALUE, value: 'special value 1', icon: null, documentation: 'Description for value 1', children: [], }, { title: '', type: ItemType.TAG_VALUE, value: 'special value 2', icon: null, documentation: 'Description for value 2', children: [], }, ], }, async_values: { key: 'async_values', name: 'async_values', kind: FieldKind.FIELD, predefined: false, }, }; return (

To guide the user in building a search query, filter value suggestions can be provided in a few different ways:

); }); story('Customizing the filter key menu', () => { return (

A special menu can be displayed when no text is entered in the search input, allowing for better oranization and discovery of filter keys.

This menu is defined by filterKeySections, which accepts a list of sections. Each section contains a name and a list of filter keys. Note that the order of both the sections and the items within each section are respected.

); }); story('Field definitions', () => { return (

Field definitions very important for the search query builder to work correctly. They provide information such as what data types are allow for a given filter, as well as the description and keywords.

By default, field definitions are sourced from{' '} EVENT_FIELD_DEFINITIONS in sentry/utils/fields.ts. If these definitions are not correct for the use case, they can be overridden by passing fieldDefinitionGetter.

{ return { desc: 'Customized field defintion', kind: FieldKind.FIELD, valueType: FieldValueType.BOOLEAN, }; }} searchSource="storybook" />
); }); story('Callbacks', () => { const [onChangeValue, setOnChangeValue] = useState(''); const [onSearchValue, setOnSearchValue] = useState(''); return (

onChange is called whenever the search query changes. This can be used to update the UI as the user updates the query.

onSearch is called when the user presses enter. This can be used to submit the search query.

  • Last onChange value : {onChangeValue}
  • Last onSearch value : {onSearchValue}

); }); story('Configuring valid syntax', () => { const configs = [ 'disallowFreeText', 'disallowLogicalOperators', 'disallowWildcard', 'disallowUnsupportedFilters', ]; const [enabledConfigs, setEnabledConfigs] = useState([...configs]); const queryBuilderOptions = enabledConfigs.reduce((acc, config) => { acc[config] = true; return acc; }, {}); return (

There are some config options which allow you to customize which types of syntax are considered valid. This should be used when the search backend does not support certain operators like boolean logic or wildcards. Use the checkboxes below to enable/disable the following options:

{configs.map(config => ( {config} ))}

The query above has a few invalid tokens. The invalid tokens are highlighted in red and display a tooltip with a message when focused. The invalid token messages can be customized using the invalidMessages prop. In this case, the unsupported tag message is modified with{' '} .

); }); story('Disabled', () => { return ( ); }); story('Migrating from SmartSearchBar', () => { return (

is a replacement for{' '} . It provides a more flexible and powerful search query builder.

Some props have been renamed:

  • supportedTags {'->'} filterKeys
  • onGetTagValues {'->'} getTagValues
  • highlightUnsupportedTags {'->'}{' '} disallowUnsupportedFilters
  • savedSearchType {'->'} recentSearches

Some props have been removed:

  • excludedTags is no longer supported. If a filter key should not be shown, do not include it in filterKeys.
  • (boolean|date|duration)Keys no longer need to be specified. The filter value types are inferred from the field definitions.
  • projectIds was used to add is_multi_project to some of the analytics events. If your use case requires this, you can record these events manually with the onSearch callback.
  • hasRecentSearches is no longer required. Saved searches will be saved and displayed when recentSearches is provided.

); }); });