issues.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import type {Client} from 'sentry/api';
  2. import {joinQuery, parseSearch, Token} from 'sentry/components/searchSyntax/parser';
  3. import {t} from 'sentry/locale';
  4. import GroupStore from 'sentry/stores/groupStore';
  5. import type {Group, Organization, PageFilters} from 'sentry/types';
  6. import {getIssueFieldRenderer} from 'sentry/utils/dashboards/issueFieldRenderers';
  7. import {getUtcDateString} from 'sentry/utils/dates';
  8. import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
  9. import type {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl';
  10. import {
  11. DISCOVER_EXCLUSION_FIELDS,
  12. getSortLabel,
  13. IssueSortOptions,
  14. } from 'sentry/views/issueList/utils';
  15. import type {Widget, WidgetQuery} from '../types';
  16. import {DEFAULT_TABLE_LIMIT, DisplayType} from '../types';
  17. import {IssuesSearchBar} from '../widgetBuilder/buildSteps/filterResultsStep/issuesSearchBar';
  18. import {ISSUE_FIELD_TO_HEADER_MAP} from '../widgetBuilder/issueWidget/fields';
  19. import {generateIssueWidgetFieldOptions} from '../widgetBuilder/issueWidget/utils';
  20. import type {DatasetConfig} from './base';
  21. const DEFAULT_WIDGET_QUERY: WidgetQuery = {
  22. name: '',
  23. fields: ['issue', 'assignee', 'title'] as string[],
  24. columns: ['issue', 'assignee', 'title'],
  25. fieldAliases: [],
  26. aggregates: [],
  27. conditions: '',
  28. orderby: IssueSortOptions.DATE,
  29. };
  30. const DEFAULT_SORT = IssueSortOptions.DATE;
  31. const DEFAULT_EXPAND = ['owners'];
  32. type EndpointParams = Partial<PageFilters['datetime']> & {
  33. environment: string[];
  34. project: number[];
  35. collapse?: string[];
  36. cursor?: string;
  37. expand?: string[];
  38. groupStatsPeriod?: string | null;
  39. limit?: number;
  40. page?: number | string;
  41. query?: string;
  42. sort?: string;
  43. statsPeriod?: string | null;
  44. };
  45. export const IssuesConfig: DatasetConfig<never, Group[]> = {
  46. defaultWidgetQuery: DEFAULT_WIDGET_QUERY,
  47. enableEquations: false,
  48. disableSortOptions,
  49. getTableRequest,
  50. getCustomFieldRenderer: getIssueFieldRenderer,
  51. SearchBar: IssuesSearchBar,
  52. getTableSortOptions,
  53. getTableFieldOptions: (_organization: Organization) =>
  54. generateIssueWidgetFieldOptions(),
  55. getFieldHeaderMap: () => ISSUE_FIELD_TO_HEADER_MAP,
  56. supportedDisplayTypes: [DisplayType.TABLE],
  57. transformTable: transformIssuesResponseToTable,
  58. };
  59. function disableSortOptions(_widgetQuery: WidgetQuery) {
  60. return {
  61. disableSort: false,
  62. disableSortDirection: true,
  63. disableSortReason: t('Issues dataset does not yet support descending order'),
  64. };
  65. }
  66. function getTableSortOptions(_organization: Organization, _widgetQuery: WidgetQuery) {
  67. const sortOptions = [
  68. IssueSortOptions.DATE,
  69. IssueSortOptions.NEW,
  70. IssueSortOptions.PRIORITY,
  71. IssueSortOptions.FREQ,
  72. IssueSortOptions.USER,
  73. ];
  74. return sortOptions.map(sortOption => ({
  75. label: getSortLabel(sortOption),
  76. value: sortOption,
  77. }));
  78. }
  79. export function transformIssuesResponseToTable(
  80. data: Group[],
  81. widgetQuery: WidgetQuery,
  82. _organization: Organization,
  83. pageFilters: PageFilters
  84. ): TableData {
  85. GroupStore.add(data);
  86. const transformedTableResults: TableDataRow[] = [];
  87. data.forEach(
  88. ({
  89. id,
  90. shortId,
  91. title,
  92. lifetime,
  93. filtered,
  94. count,
  95. userCount,
  96. project,
  97. annotations,
  98. ...resultProps
  99. }) => {
  100. const transformedResultProps: Omit<TableDataRow, 'id'> = {};
  101. Object.keys(resultProps)
  102. .filter(key => ['number', 'string'].includes(typeof resultProps[key]))
  103. .forEach(key => {
  104. transformedResultProps[key] = resultProps[key];
  105. });
  106. const transformedTableResult: TableDataRow = {
  107. ...transformedResultProps,
  108. events: count,
  109. users: userCount,
  110. id,
  111. 'issue.id': id,
  112. issue: shortId,
  113. title,
  114. project: project.slug,
  115. links: annotations?.join(', '),
  116. };
  117. // Get lifetime stats
  118. if (lifetime) {
  119. transformedTableResult.lifetimeEvents = lifetime?.count;
  120. transformedTableResult.lifetimeUsers = lifetime?.userCount;
  121. }
  122. // Get filtered stats
  123. if (filtered) {
  124. transformedTableResult.filteredEvents = filtered?.count;
  125. transformedTableResult.filteredUsers = filtered?.userCount;
  126. }
  127. // Discover Url properties
  128. const query = widgetQuery.conditions;
  129. const parsedResult = parseSearch(query);
  130. const filteredTerms = parsedResult?.filter(
  131. p => !(p.type === Token.FILTER && DISCOVER_EXCLUSION_FIELDS.includes(p.key.text))
  132. );
  133. transformedTableResult.discoverSearchQuery = joinQuery(filteredTerms, true);
  134. transformedTableResult.projectId = project.id;
  135. const {period, start, end} = pageFilters.datetime || {};
  136. if (start && end) {
  137. transformedTableResult.start = getUtcDateString(start);
  138. transformedTableResult.end = getUtcDateString(end);
  139. }
  140. transformedTableResult.period = period ?? '';
  141. transformedTableResults.push(transformedTableResult);
  142. }
  143. );
  144. return {data: transformedTableResults} as TableData;
  145. }
  146. function getTableRequest(
  147. api: Client,
  148. _: Widget,
  149. query: WidgetQuery,
  150. organization: Organization,
  151. pageFilters: PageFilters,
  152. __?: OnDemandControlContext,
  153. limit?: number,
  154. cursor?: string
  155. ) {
  156. const groupListUrl = `/organizations/${organization.slug}/issues/`;
  157. const params: EndpointParams = {
  158. project: pageFilters.projects ?? [],
  159. environment: pageFilters.environments ?? [],
  160. query: query.conditions,
  161. sort: query.orderby || DEFAULT_SORT,
  162. expand: DEFAULT_EXPAND,
  163. limit: limit ?? DEFAULT_TABLE_LIMIT,
  164. cursor,
  165. };
  166. if (pageFilters.datetime.period) {
  167. params.statsPeriod = pageFilters.datetime.period;
  168. }
  169. if (pageFilters.datetime.end) {
  170. params.end = getUtcDateString(pageFilters.datetime.end);
  171. }
  172. if (pageFilters.datetime.start) {
  173. params.start = getUtcDateString(pageFilters.datetime.start);
  174. }
  175. if (pageFilters.datetime.utc) {
  176. params.utc = pageFilters.datetime.utc;
  177. }
  178. return api.requestPromise(groupListUrl, {
  179. includeAllArgs: true,
  180. method: 'GET',
  181. data: {
  182. ...params,
  183. },
  184. });
  185. }