import {Fragment, useState} from 'react'; import MultipleCheckbox from 'sentry/components/forms/controls/multipleCheckbox'; import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; import {FormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery'; import type { FieldDefinitionGetter, 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, getFieldDefinition, 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', 'Internet Explorer', 'Opera 1,2'], }, [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 FILTER_KEY_SECTIONS: FilterKeySection[] = [ { value: 'cat_1', label: 'Category 1', children: [FieldKey.ASSIGNED, FieldKey.IS], }, { value: 'cat_2', label: 'Category 2', children: [WebVital.LCP, MobileVital.FRAMES_SLOW_RATE], }, { value: 'cat_3', label: 'Category 3', children: [FieldKey.TIMES_SEEN], }, { value: 'cat_4', label: 'Category 4', children: [FieldKey.LAST_SEEN, FieldKey.TIMES_SEEN], }, { value: 'cat_5', label: 'Category 5', 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 organization 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.

If you wish to modify the size of the filter key menu, use filterKeyMenuWidth to define the width in pixels.

); }); 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 definition', kind: FieldKind.FIELD, valueType: FieldValueType.BOOLEAN, }; }} searchSource="storybook" />
); }); story('Aggregate filters', () => { const aggregateFilterKeys: TagCollection = { apdex: { key: 'apdex', name: 'apdex', kind: FieldKind.FUNCTION, }, count: { key: 'count', name: 'count', kind: FieldKind.FUNCTION, }, count_if: { key: 'count_if', name: 'count_if', kind: FieldKind.FUNCTION, }, p95: { key: 'p95', name: 'p95', kind: FieldKind.FUNCTION, }, 'transaction.duration': { key: 'transaction.duration', name: 'transaction.duration', kind: FieldKind.FIELD, }, timesSeen: { key: 'timesSeen', name: 'timesSeen', kind: FieldKind.FIELD, }, lastSeen: { key: 'lastSeen', name: 'lastSeen', kind: FieldKind.FIELD, }, }; const getAggregateFieldDefinition: FieldDefinitionGetter = (key: string) => { switch (key) { case 'apdex': return { desc: 'Returns results with the Apdex score that you entered. Values must be between 0 and 1. Higher apdex values indicate higher user satisfaction.', kind: FieldKind.FUNCTION, valueType: FieldValueType.NUMBER, parameters: [ { name: 'threshold', kind: 'value' as const, dataType: FieldValueType.NUMBER, defaultValue: '300', required: true, }, ], }; case 'count': return { desc: 'Returns results with a matching count.', kind: FieldKind.FUNCTION, valueType: FieldValueType.INTEGER, parameters: [], }; case 'count_if': return { desc: 'Returns results with a matching count that satisfy the condition passed to the parameters of the function.', kind: FieldKind.FUNCTION, valueType: FieldValueType.INTEGER, parameters: [ { name: 'column', kind: 'column' as const, columnTypes: [ FieldValueType.STRING, FieldValueType.NUMBER, FieldValueType.DURATION, ], defaultValue: 'transaction.duration', required: true, }, { name: 'operator', kind: 'value' as const, options: [ { label: 'is equal to', value: 'equals', }, { label: 'is not equal to', value: 'notEquals', }, { label: 'is less than', value: 'less', }, { label: 'is greater than', value: 'greater', }, { label: 'is less than or equal to', value: 'lessOrEquals', }, { label: 'is greater than or equal to', value: 'greaterOrEquals', }, ], dataType: FieldValueType.STRING, defaultValue: 'equals', required: true, }, { name: 'value', kind: 'value', dataType: FieldValueType.STRING, defaultValue: '300ms', required: true, }, ], }; case 'p95': return { desc: 'Returns results with the 95th percentile of the selected column.', kind: FieldKind.FUNCTION, defaultValue: '300ms', valueType: null, parameterDependentValueType: parameters => { const column = parameters[0]; const fieldDef = column ? getFieldDefinition(column) : null; return fieldDef?.valueType ?? FieldValueType.NUMBER; }, parameters: [ { name: 'column', kind: 'column' as const, columnTypes: [ FieldValueType.DURATION, FieldValueType.NUMBER, FieldValueType.INTEGER, FieldValueType.PERCENTAGE, ], defaultValue: 'transaction.duration', required: true, }, ], }; default: return getFieldDefinition(key); } }; return (

Filter keys can be defined as aggregate filters, which allow for more complex operations. They may accept any number of parameters, which are defined in the field definition.

To define an aggregate filter, set the kind to{' '} FieldKind.FUNCTION, and the valueType to the return type of the function. Then define the parameters, which is an array of acceptable column types or a predicate function.

Some aggreate filters may have a return type that is dependent on the parameters. For example, p95(column) may return a few different types depending on the column type. In this case, the field definition should implement parameterDependentValueType. This function accepts an array of parameters and returns the value type.

); }); 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('Unsubmitted search indicator', () => { const [query, setQuery] = useState('is:unresolved assigned:me'); return (

You can display an indicator when the search query has been modified but not fully submitted using the showUnsubmittedIndicator prop. This can be useful to remind the user that they have unsaved changes for use cases which require manual submission.

Current query: {query}

); }); story('Disabled', () => { return ( ); }); story('FormattedQuery', () => { return (

If you just need to render a formatted query outside of the search bar,{' '} is exported for this purpose:

); }); 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.

); }); });