issues.tsx 6.3 KB

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