widgetViewerTableCell.tsx 11 KB

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