issueWidgetQueriesForm.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import * as React from 'react';
  2. import styled from '@emotion/styled';
  3. import cloneDeep from 'lodash/cloneDeep';
  4. import {fetchTagValues} from 'sentry/actionCreators/tags';
  5. import {Client} from 'sentry/api';
  6. import SelectControl from 'sentry/components/forms/selectControl';
  7. import {t} from 'sentry/locale';
  8. import space from 'sentry/styles/space';
  9. import {Organization, PageFilters, SelectValue, TagCollection} from 'sentry/types';
  10. import {getUtcDateString} from 'sentry/utils/dates';
  11. import {explodeField, generateFieldAsString} from 'sentry/utils/discover/fields';
  12. import withApi from 'sentry/utils/withApi';
  13. import withIssueTags from 'sentry/utils/withIssueTags';
  14. import {DisplayType, WidgetQuery, WidgetType} from 'sentry/views/dashboardsV2/types';
  15. import {generateIssueWidgetOrderOptions} from 'sentry/views/dashboardsV2/widget/issueWidget/utils';
  16. import {generateFieldOptions} from 'sentry/views/eventsV2/utils';
  17. import IssueListSearchBar from 'sentry/views/issueList/searchBar';
  18. import {IssueSortOptions} from 'sentry/views/issueList/utils';
  19. import Field from 'sentry/views/settings/components/forms/field';
  20. import WidgetQueryFields from './widgetQueryFields';
  21. type Props = {
  22. api: Client;
  23. organization: Organization;
  24. selection: PageFilters;
  25. query: WidgetQuery;
  26. error?: Record<string, any>;
  27. onChange: (widgetQuery: WidgetQuery) => void;
  28. tags: TagCollection;
  29. fieldOptions: ReturnType<typeof generateFieldOptions>;
  30. };
  31. type State = {
  32. blurTimeout?: ReturnType<typeof setTimeout>;
  33. };
  34. /**
  35. * Contain widget queries interactions and signal changes via the onChange
  36. * callback. This component's state should live in the parent.
  37. */
  38. class IssueWidgetQueriesForm extends React.Component<Props, State> {
  39. constructor(props: Props) {
  40. super(props);
  41. this.state = {
  42. blurTimeout: undefined,
  43. };
  44. }
  45. // Handle scalar field values changing.
  46. handleFieldChange = (field: string) => {
  47. const {query, onChange} = this.props;
  48. const widgetQuery = query;
  49. return function handleChange(value: string) {
  50. const newQuery = {...widgetQuery, [field]: value};
  51. onChange(newQuery);
  52. };
  53. };
  54. tagValueLoader = (key: string, search: string) => {
  55. const {organization, selection, api} = this.props;
  56. const orgId = organization.slug;
  57. const projectIds = selection.projects.map(id => id.toString());
  58. const endpointParams = {
  59. start: getUtcDateString(selection.datetime.start),
  60. end: getUtcDateString(selection.datetime.end),
  61. statsPeriod: selection.datetime.period,
  62. };
  63. return fetchTagValues(api, orgId, key, search, projectIds, endpointParams);
  64. };
  65. render() {
  66. const {organization, error, query, tags, fieldOptions, onChange} = this.props;
  67. const explodedFields = query.fields.map(field => explodeField({field}));
  68. const {blurTimeout} = this.state;
  69. return (
  70. <QueryWrapper>
  71. <Field
  72. label={t('Query')}
  73. inline={false}
  74. style={{paddingBottom: `8px`}}
  75. flexibleControlStateSize
  76. stacked
  77. error={error?.conditions}
  78. >
  79. <SearchConditionsWrapper>
  80. <StyledIssueListSearchBar
  81. organization={organization}
  82. query={query.conditions || ''}
  83. sort=""
  84. onSearch={field => {
  85. // IssueListSearchBar will call handlers for both onSearch and onBlur
  86. // when selecting a value from the autocomplete dropdown. This can
  87. // cause state issues for the search bar in our use case. To prevent
  88. // this, we set a timer in our onSearch handler to block our onBlur
  89. // handler from firing if it is within 200ms, ie from clicking an
  90. // autocomplete value.
  91. this.setState({
  92. blurTimeout: setTimeout(() => {
  93. this.setState({blurTimeout: undefined});
  94. }, 200),
  95. });
  96. return this.handleFieldChange('conditions')(field);
  97. }}
  98. onBlur={field => {
  99. if (!blurTimeout) {
  100. this.handleFieldChange('conditions')(field);
  101. }
  102. }}
  103. excludeEnvironment
  104. supportedTags={tags}
  105. tagValueLoader={this.tagValueLoader}
  106. onSidebarToggle={() => undefined}
  107. />
  108. </SearchConditionsWrapper>
  109. </Field>
  110. <WidgetQueryFields
  111. widgetType={WidgetType.ISSUE}
  112. displayType={DisplayType.TABLE}
  113. fieldOptions={fieldOptions}
  114. errors={error}
  115. fields={explodedFields}
  116. organization={organization}
  117. onChange={fields => {
  118. const fieldStrings = fields.map(field => generateFieldAsString(field));
  119. const newQuery = cloneDeep(query);
  120. newQuery.fields = fieldStrings;
  121. onChange(newQuery);
  122. }}
  123. />
  124. <Field
  125. label={t('Sort by')}
  126. inline={false}
  127. flexibleControlStateSize
  128. stacked
  129. error={error?.orderby}
  130. style={{marginBottom: space(1)}}
  131. >
  132. <SelectControl
  133. value={query.orderby || IssueSortOptions.DATE}
  134. name="orderby"
  135. options={generateIssueWidgetOrderOptions(
  136. organization?.features?.includes('issue-list-trend-sort')
  137. )}
  138. onChange={(option: SelectValue<string>) =>
  139. this.handleFieldChange('orderby')(option.value)
  140. }
  141. />
  142. </Field>
  143. </QueryWrapper>
  144. );
  145. }
  146. }
  147. const QueryWrapper = styled('div')`
  148. position: relative;
  149. padding-bottom: 16px;
  150. `;
  151. export const SearchConditionsWrapper = styled('div')`
  152. display: flex;
  153. align-items: center;
  154. > * + * {
  155. margin-left: ${space(1)};
  156. }
  157. `;
  158. const StyledIssueListSearchBar = styled(IssueListSearchBar)`
  159. flex-grow: 1;
  160. button:not([aria-label='Clear search']) {
  161. display: none;
  162. }
  163. `;
  164. export default withApi(withIssueTags(IssueWidgetQueriesForm));