widgetViewerTableCell.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location, LocationDescriptorObject} from 'history';
  4. import trimStart from 'lodash/trimStart';
  5. import {GridColumnOrder} from 'sentry/components/gridEditable';
  6. import SortLink from 'sentry/components/gridEditable/sortLink';
  7. import Link from 'sentry/components/links/link';
  8. import Tooltip from 'sentry/components/tooltip';
  9. import {t} from 'sentry/locale';
  10. import {Organization, PageFilters} from 'sentry/types';
  11. import {defined} from 'sentry/utils';
  12. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  13. import {
  14. getIssueFieldRenderer,
  15. getSortField,
  16. } from 'sentry/utils/dashboards/issueFieldRenderers';
  17. import {TableDataRow, TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
  18. import EventView, {isFieldSortable} from 'sentry/utils/discover/eventView';
  19. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  20. import {
  21. fieldAlignment,
  22. getAggregateAlias,
  23. getEquationAliasIndex,
  24. isAggregateField,
  25. isEquationAlias,
  26. Sort,
  27. } from 'sentry/utils/discover/fields';
  28. import {
  29. eventDetailsRouteWithEventView,
  30. generateEventSlug,
  31. } from 'sentry/utils/discover/urls';
  32. import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboardsV2/types';
  33. import {eventViewFromWidget} from 'sentry/views/dashboardsV2/utils';
  34. import {ISSUE_FIELDS} from 'sentry/views/dashboardsV2/widgetBuilder/issueWidget/fields';
  35. import TopResultsIndicator from 'sentry/views/eventsV2/table/topResultsIndicator';
  36. import {TableColumn} from 'sentry/views/eventsV2/table/types';
  37. import {WidgetViewerQueryField} from './utils';
  38. // Dashboards only supports top 5 for now
  39. const DEFAULT_NUM_TOP_EVENTS = 5;
  40. type Props = {
  41. location: Location;
  42. organization: Organization;
  43. selection: PageFilters;
  44. widget: Widget;
  45. isFirstPage?: boolean;
  46. onHeaderClick?: () => void;
  47. tableData?: TableDataWithTitle;
  48. };
  49. export const renderIssueGridHeaderCell =
  50. ({location, widget, tableData, organization, onHeaderClick}: Props) =>
  51. (column: TableColumn<keyof TableDataRow>, _columnIndex: number): React.ReactNode => {
  52. const tableMeta = tableData?.meta;
  53. const align = fieldAlignment(column.name, column.type, tableMeta);
  54. const sortField = getSortField(String(column.key));
  55. return (
  56. <SortLink
  57. align={align}
  58. title={<StyledTooltip title={column.name}>{column.name}</StyledTooltip>}
  59. direction={widget.queries[0].orderby === sortField ? 'desc' : undefined}
  60. canSort={!!sortField}
  61. generateSortLink={() => ({
  62. ...location,
  63. query: {
  64. ...location.query,
  65. [WidgetViewerQueryField.SORT]: sortField,
  66. [WidgetViewerQueryField.PAGE]: undefined,
  67. [WidgetViewerQueryField.CURSOR]: undefined,
  68. },
  69. })}
  70. onClick={() => {
  71. onHeaderClick?.();
  72. trackAdvancedAnalyticsEvent('dashboards_views.widget_viewer.sort', {
  73. organization,
  74. widget_type: WidgetType.ISSUE,
  75. display_type: widget.displayType,
  76. column: column.name,
  77. order: 'desc',
  78. });
  79. }}
  80. />
  81. );
  82. };
  83. export const renderDiscoverGridHeaderCell =
  84. ({location, selection, widget, tableData, organization, onHeaderClick}: Props) =>
  85. (column: TableColumn<keyof TableDataRow>, _columnIndex: number): React.ReactNode => {
  86. const {orderby} = widget.queries[0];
  87. // Need to convert orderby to aggregate alias because eventView still uses aggregate alias format
  88. const aggregateAliasOrderBy = `${
  89. orderby.startsWith('-') ? '-' : ''
  90. }${getAggregateAlias(trimStart(orderby, '-'))}`;
  91. const eventView = eventViewFromWidget(
  92. widget.title,
  93. {...widget.queries[0], orderby: aggregateAliasOrderBy},
  94. selection,
  95. widget.displayType
  96. );
  97. const tableMeta = tableData?.meta;
  98. const align = fieldAlignment(column.name, column.type, tableMeta);
  99. const field = {field: String(column.key), width: column.width};
  100. function generateSortLink(): LocationDescriptorObject | undefined {
  101. if (!tableMeta) {
  102. return undefined;
  103. }
  104. const nextEventView = eventView.sortOnField(field, tableMeta, undefined, true);
  105. const queryStringObject = nextEventView.generateQueryStringObject();
  106. return {
  107. ...location,
  108. query: {
  109. ...location.query,
  110. [WidgetViewerQueryField.SORT]: queryStringObject.sort,
  111. [WidgetViewerQueryField.PAGE]: undefined,
  112. [WidgetViewerQueryField.CURSOR]: undefined,
  113. },
  114. };
  115. }
  116. const currentSort = eventView.sortForField(field, tableMeta);
  117. const canSort = isFieldSortable(field, tableMeta);
  118. const titleText = isEquationAlias(column.name)
  119. ? eventView.getEquations()[getEquationAliasIndex(column.name)]
  120. : column.name;
  121. return (
  122. <SortLink
  123. align={align}
  124. title={<StyledTooltip title={titleText}>{titleText}</StyledTooltip>}
  125. direction={currentSort ? currentSort.kind : undefined}
  126. canSort={canSort}
  127. generateSortLink={generateSortLink}
  128. onClick={() => {
  129. onHeaderClick?.();
  130. trackAdvancedAnalyticsEvent('dashboards_views.widget_viewer.sort', {
  131. organization,
  132. widget_type: WidgetType.DISCOVER,
  133. display_type: widget.displayType,
  134. column: column.name,
  135. order: currentSort?.kind === 'desc' ? 'asc' : 'desc',
  136. });
  137. }}
  138. />
  139. );
  140. };
  141. export const renderGridBodyCell =
  142. ({location, organization, widget, tableData, isFirstPage}: Props) =>
  143. (
  144. column: GridColumnOrder,
  145. dataRow: Record<string, any>,
  146. rowIndex: number,
  147. columnIndex: number
  148. ): React.ReactNode => {
  149. const columnKey = String(column.key);
  150. const isTopEvents = widget.displayType === DisplayType.TOP_N;
  151. let cell: React.ReactNode;
  152. const isAlias =
  153. !organization.features.includes('discover-frontend-use-events-endpoint') &&
  154. widget.widgetType !== WidgetType.RELEASE;
  155. switch (widget.widgetType) {
  156. case WidgetType.ISSUE:
  157. cell = (
  158. getIssueFieldRenderer(columnKey) ?? getFieldRenderer(columnKey, ISSUE_FIELDS)
  159. )(dataRow, {organization, location});
  160. break;
  161. case WidgetType.DISCOVER:
  162. default:
  163. if (!tableData || !tableData.meta) {
  164. return dataRow[column.key];
  165. }
  166. const unit = tableData.meta.units?.[column.key];
  167. cell = getFieldRenderer(
  168. columnKey,
  169. tableData.meta,
  170. isAlias
  171. )(dataRow, {
  172. organization,
  173. location,
  174. unit,
  175. });
  176. const fieldName = getAggregateAlias(columnKey);
  177. const value = dataRow[fieldName];
  178. if (tableData.meta[fieldName] === 'integer' && defined(value) && value > 999) {
  179. return (
  180. <Tooltip
  181. title={value.toLocaleString()}
  182. containerDisplayMode="block"
  183. position="right"
  184. >
  185. {cell}
  186. </Tooltip>
  187. );
  188. }
  189. break;
  190. }
  191. const topResultsCount = tableData
  192. ? Math.min(tableData?.data.length, DEFAULT_NUM_TOP_EVENTS)
  193. : DEFAULT_NUM_TOP_EVENTS;
  194. return (
  195. <Fragment>
  196. {isTopEvents &&
  197. isFirstPage &&
  198. rowIndex < DEFAULT_NUM_TOP_EVENTS &&
  199. columnIndex === 0 ? (
  200. <TopResultsIndicator count={topResultsCount} index={rowIndex} />
  201. ) : null}
  202. {cell}
  203. </Fragment>
  204. );
  205. };
  206. export const renderPrependColumns =
  207. ({location, organization, tableData, eventView}: Props & {eventView: EventView}) =>
  208. (isHeader: boolean, dataRow?: any, rowIndex?: number): React.ReactNode[] => {
  209. if (isHeader) {
  210. return [
  211. <PrependHeader key="header-event-id">
  212. <SortLink
  213. align="left"
  214. title={t('event id')}
  215. direction={undefined}
  216. canSort={false}
  217. generateSortLink={() => undefined}
  218. />
  219. </PrependHeader>,
  220. ];
  221. }
  222. let value = dataRow.id;
  223. if (tableData?.meta) {
  224. const fieldRenderer = getFieldRenderer('id', tableData?.meta);
  225. value = fieldRenderer(dataRow, {organization, location});
  226. }
  227. const eventSlug = generateEventSlug(dataRow);
  228. const target = eventDetailsRouteWithEventView({
  229. orgSlug: organization.slug,
  230. eventSlug,
  231. eventView,
  232. });
  233. return [
  234. <Tooltip key={`eventlink${rowIndex}`} title={t('View Event')}>
  235. <Link data-test-id="view-event" to={target}>
  236. {value}
  237. </Link>
  238. </Tooltip>,
  239. ];
  240. };
  241. export const renderReleaseGridHeaderCell =
  242. ({location, widget, tableData, organization, onHeaderClick}: Props) =>
  243. (column: TableColumn<keyof TableDataRow>, _columnIndex: number): React.ReactNode => {
  244. const tableMeta = tableData?.meta;
  245. const align = fieldAlignment(column.name, column.type, tableMeta);
  246. const widgetOrderBy = widget.queries[0].orderby;
  247. const sort: Sort = {
  248. kind: widgetOrderBy.startsWith('-') ? 'desc' : 'asc',
  249. field: widgetOrderBy.startsWith('-') ? widgetOrderBy.slice(1) : widgetOrderBy,
  250. };
  251. const canSort = isAggregateField(column.name);
  252. const titleText = column.name;
  253. function generateSortLink(): LocationDescriptorObject {
  254. const columnSort =
  255. column.name === sort.field
  256. ? {...sort, kind: sort.kind === 'desc' ? 'asc' : 'desc'}
  257. : {kind: 'desc', field: column.name};
  258. return {
  259. ...location,
  260. query: {
  261. ...location.query,
  262. [WidgetViewerQueryField.SORT]:
  263. columnSort.kind === 'desc' ? `-${columnSort.field}` : columnSort.field,
  264. [WidgetViewerQueryField.PAGE]: undefined,
  265. [WidgetViewerQueryField.CURSOR]: undefined,
  266. },
  267. };
  268. }
  269. return (
  270. <SortLink
  271. align={align}
  272. title={<StyledTooltip title={titleText}>{titleText}</StyledTooltip>}
  273. direction={sort.field === column.name ? sort.kind : undefined}
  274. canSort={canSort}
  275. generateSortLink={generateSortLink}
  276. onClick={() => {
  277. onHeaderClick?.();
  278. trackAdvancedAnalyticsEvent('dashboards_views.widget_viewer.sort', {
  279. organization,
  280. widget_type: WidgetType.RELEASE,
  281. display_type: widget.displayType,
  282. column: column.name,
  283. order: sort?.kind === 'desc' ? 'asc' : 'desc',
  284. });
  285. }}
  286. />
  287. );
  288. };
  289. const StyledTooltip = styled(Tooltip)`
  290. display: initial;
  291. `;
  292. const PrependHeader = styled('span')`
  293. color: ${p => p.theme.subText};
  294. `;