widgetViewerTableCell.tsx 10 KB

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