issueWidgetQueriesForm.tsx 5.3 KB

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