widgetViewerTableCell.tsx 11 KB

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