tableView.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import type {Location, LocationDescriptorObject} from 'history';
  5. import {openModal} from 'sentry/actionCreators/modal';
  6. import GridEditable, {
  7. COL_WIDTH_MINIMUM,
  8. COL_WIDTH_UNDEFINED,
  9. } from 'sentry/components/gridEditable';
  10. import SortLink from 'sentry/components/gridEditable/sortLink';
  11. import Link from 'sentry/components/links/link';
  12. import {Tooltip} from 'sentry/components/tooltip';
  13. import Truncate from 'sentry/components/truncate';
  14. import {IconStack} from 'sentry/icons';
  15. import {t} from 'sentry/locale';
  16. import type {Organization} from 'sentry/types/organization';
  17. import {trackAnalytics} from 'sentry/utils/analytics';
  18. import {browserHistory} from 'sentry/utils/browserHistory';
  19. import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
  20. import {getTimeStampFromTableDateField} from 'sentry/utils/dates';
  21. import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
  22. import type EventView from 'sentry/utils/discover/eventView';
  23. import {
  24. isFieldSortable,
  25. pickRelevantLocationQueryStrings,
  26. } from 'sentry/utils/discover/eventView';
  27. import {
  28. DURATION_UNITS,
  29. getFieldRenderer,
  30. SIZE_UNITS,
  31. } from 'sentry/utils/discover/fieldRenderers';
  32. import type {Column} from 'sentry/utils/discover/fields';
  33. import {
  34. fieldAlignment,
  35. getEquationAliasIndex,
  36. isEquationAlias,
  37. } from 'sentry/utils/discover/fields';
  38. import {
  39. type DiscoverDatasets,
  40. DisplayModes,
  41. SavedQueryDatasets,
  42. TOP_N,
  43. } from 'sentry/utils/discover/types';
  44. import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
  45. import ViewReplayLink from 'sentry/utils/discover/viewReplayLink';
  46. import {getShortEventId} from 'sentry/utils/events';
  47. import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
  48. import {decodeList} from 'sentry/utils/queryString';
  49. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  50. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  51. import useProjects from 'sentry/utils/useProjects';
  52. import {useRoutes} from 'sentry/utils/useRoutes';
  53. import {appendQueryDatasetParam, hasDatasetSelector} from 'sentry/views/dashboards/utils';
  54. import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceMetadataHeader';
  55. import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
  56. import {generateReplayLink} from 'sentry/views/performance/transactionSummary/utils';
  57. import {
  58. getExpandedResults,
  59. getTargetForTransactionSummaryLink,
  60. pushEventViewToLocation,
  61. } from '../utils';
  62. import {QuickContextHoverWrapper} from './quickContext/quickContextWrapper';
  63. import {ContextType} from './quickContext/utils';
  64. import CellAction, {Actions, updateQuery} from './cellAction';
  65. import ColumnEditModal, {modalCss} from './columnEditModal';
  66. import TableActions from './tableActions';
  67. import TopResultsIndicator from './topResultsIndicator';
  68. import type {TableColumn} from './types';
  69. export type TableViewProps = {
  70. error: string | null;
  71. eventView: EventView;
  72. isFirstPage: boolean;
  73. isLoading: boolean;
  74. location: Location;
  75. measurementKeys: null | string[];
  76. onChangeShowTags: () => void;
  77. organization: Organization;
  78. showTags: boolean;
  79. tableData: TableData | null | undefined;
  80. title: string;
  81. customMeasurements?: CustomMeasurementCollection;
  82. dataset?: DiscoverDatasets;
  83. isHomepage?: boolean;
  84. queryDataset?: SavedQueryDatasets;
  85. spanOperationBreakdownKeys?: string[];
  86. };
  87. /**
  88. * The `TableView` is marked with leading _ in its method names. It consumes
  89. * the EventView object given in its props to generate new EventView objects
  90. * for actions like resizing column.
  91. *
  92. * The entire state of the table view (or event view) is co-located within
  93. * the EventView object. This object is fed from the props.
  94. *
  95. * Attempting to modify the state, and therefore, modifying the given EventView
  96. * object given from its props, will generate new instances of EventView objects.
  97. *
  98. * In most cases, the new EventView object differs from the previous EventView
  99. * object. The new EventView object is pushed to the location object.
  100. */
  101. function TableView(props: TableViewProps) {
  102. const {projects} = useProjects();
  103. const routes = useRoutes();
  104. const replayLinkGenerator = generateReplayLink(routes);
  105. /**
  106. * Updates a column on resizing
  107. */
  108. function _resizeColumn(
  109. columnIndex: number,
  110. nextColumn: TableColumn<keyof TableDataRow>
  111. ) {
  112. const {location, eventView} = props;
  113. const newWidth = nextColumn.width ? Number(nextColumn.width) : COL_WIDTH_UNDEFINED;
  114. const nextEventView = eventView.withResizedColumn(columnIndex, newWidth);
  115. pushEventViewToLocation({
  116. location,
  117. nextEventView,
  118. extraQuery: pickRelevantLocationQueryStrings(location),
  119. });
  120. }
  121. function _renderPrependColumns(
  122. isHeader: boolean,
  123. dataRow?: any,
  124. rowIndex?: number
  125. ): React.ReactNode[] {
  126. const {organization, eventView, tableData, location, isHomepage, queryDataset} =
  127. props;
  128. const hasAggregates = eventView.hasAggregateField();
  129. const hasIdField = eventView.hasIdField();
  130. const isTransactionsDataset =
  131. hasDatasetSelector(organization) &&
  132. queryDataset === SavedQueryDatasets.TRANSACTIONS;
  133. if (isHeader) {
  134. if (hasAggregates) {
  135. return [
  136. <PrependHeader key="header-icon">
  137. <IconStack size="sm" />
  138. </PrependHeader>,
  139. ];
  140. }
  141. if (!hasIdField) {
  142. return [
  143. <PrependHeader key="header-event-id">
  144. <SortLink
  145. align="left"
  146. title={t('event id')}
  147. direction={undefined}
  148. canSort={false}
  149. generateSortLink={() => undefined}
  150. />
  151. </PrependHeader>,
  152. ];
  153. }
  154. return [];
  155. }
  156. if (hasAggregates) {
  157. const nextView = getExpandedResults(eventView, {}, dataRow);
  158. const target = {
  159. pathname: location.pathname,
  160. query: nextView.generateQueryStringObject(),
  161. };
  162. return [
  163. <Tooltip key={`eventlink${rowIndex}`} title={t('Open Group')}>
  164. <Link
  165. to={target}
  166. data-test-id="open-group"
  167. onClick={() => {
  168. if (nextView.isEqualTo(eventView)) {
  169. Sentry.captureException(new Error('Failed to drilldown'));
  170. }
  171. }}
  172. >
  173. <StyledIcon size="sm" />
  174. </Link>
  175. </Tooltip>,
  176. ];
  177. }
  178. if (!hasIdField) {
  179. let value = dataRow.id;
  180. if (tableData?.meta) {
  181. const fieldRenderer = getFieldRenderer('id', tableData.meta);
  182. value = fieldRenderer(dataRow, {organization, location});
  183. }
  184. let target;
  185. if (dataRow['event.type'] !== 'transaction' && !isTransactionsDataset) {
  186. const project = dataRow.project || dataRow['project.name'];
  187. target = {
  188. // NOTE: This uses a legacy redirect for project event to the issue group event link
  189. // This only works with dev-server or production.
  190. pathname: normalizeUrl(
  191. `/${organization.slug}/${project}/events/${dataRow.id}/`
  192. ),
  193. query: {...location.query, referrer: 'discover-events-table'},
  194. };
  195. } else {
  196. if (!dataRow.trace) {
  197. throw new Error(
  198. 'Transaction event should always have a trace associated with it.'
  199. );
  200. }
  201. target = generateLinkToEventInTraceView({
  202. traceSlug: dataRow.trace,
  203. eventId: dataRow.id,
  204. projectSlug: dataRow.project || dataRow['project.name'],
  205. timestamp: dataRow.timestamp,
  206. organization,
  207. isHomepage,
  208. location,
  209. eventView,
  210. type: 'discover',
  211. source: TraceViewSources.DISCOVER,
  212. });
  213. }
  214. const eventIdLink = (
  215. <StyledLink data-test-id="view-event" to={target}>
  216. {value}
  217. </StyledLink>
  218. );
  219. return [
  220. <QuickContextHoverWrapper
  221. key={`quickContextEventHover${rowIndex}`}
  222. dataRow={dataRow}
  223. contextType={ContextType.EVENT}
  224. organization={organization}
  225. projects={projects}
  226. eventView={eventView}
  227. >
  228. {eventIdLink}
  229. </QuickContextHoverWrapper>,
  230. ];
  231. }
  232. return [];
  233. }
  234. function _renderGridHeaderCell(
  235. column: TableColumn<keyof TableDataRow>
  236. ): React.ReactNode {
  237. const {eventView, location, tableData, organization, queryDataset} = props;
  238. const tableMeta = tableData?.meta;
  239. const align = fieldAlignment(column.name, column.type, tableMeta);
  240. const field = {field: column.key as string, width: column.width};
  241. function generateSortLink(): LocationDescriptorObject | undefined {
  242. if (!tableMeta) {
  243. return undefined;
  244. }
  245. const nextEventView = eventView.sortOnField(field, tableMeta);
  246. const queryStringObject = nextEventView.generateQueryStringObject();
  247. // Need to pull yAxis from location since eventView only stores 1 yAxis field at time
  248. queryStringObject.yAxis = decodeList(location.query.yAxis);
  249. return {
  250. ...location,
  251. query: {
  252. ...queryStringObject,
  253. ...appendQueryDatasetParam(organization, queryDataset),
  254. },
  255. };
  256. }
  257. const currentSort = eventView.sortForField(field, tableMeta);
  258. const canSort = isFieldSortable(field, tableMeta);
  259. let titleText = isEquationAlias(column.name)
  260. ? eventView.getEquations()[getEquationAliasIndex(column.name)]
  261. : column.name;
  262. if (column.name.toLowerCase() === 'replayid') {
  263. titleText = 'Replay';
  264. }
  265. const title = (
  266. <StyledTooltip title={titleText}>
  267. <Truncate value={titleText} maxLength={60} expandable={false} />
  268. </StyledTooltip>
  269. );
  270. return (
  271. <SortLink
  272. align={align}
  273. title={title}
  274. direction={currentSort ? currentSort.kind : undefined}
  275. canSort={canSort}
  276. generateSortLink={generateSortLink}
  277. />
  278. );
  279. }
  280. function _renderGridBodyCell(
  281. column: TableColumn<keyof TableDataRow>,
  282. dataRow: TableDataRow,
  283. rowIndex: number,
  284. columnIndex: number
  285. ): React.ReactNode {
  286. const {
  287. isFirstPage,
  288. eventView,
  289. location,
  290. organization,
  291. tableData,
  292. isHomepage,
  293. queryDataset,
  294. } = props;
  295. if (!tableData || !tableData.meta) {
  296. return dataRow[column.key];
  297. }
  298. const columnKey = String(column.key);
  299. const fieldRenderer = getFieldRenderer(columnKey, tableData.meta, false);
  300. const display = eventView.getDisplayMode();
  301. const isTopEvents =
  302. display === DisplayModes.TOP5 || display === DisplayModes.DAILYTOP5;
  303. const topEvents = eventView.topEvents ? parseInt(eventView.topEvents, 10) : TOP_N;
  304. const count = Math.min(tableData?.data?.length ?? topEvents, topEvents);
  305. const unit = tableData.meta.units?.[columnKey];
  306. let cell = fieldRenderer(dataRow, {organization, location, unit});
  307. const isTransactionsDataset =
  308. hasDatasetSelector(organization) &&
  309. queryDataset === SavedQueryDatasets.TRANSACTIONS;
  310. if (columnKey === 'id') {
  311. let target;
  312. if (dataRow['event.type'] !== 'transaction' && !isTransactionsDataset) {
  313. const project = dataRow.project || dataRow['project.name'];
  314. target = {
  315. // NOTE: This uses a legacy redirect for project event to the issue group event link.
  316. // This only works with dev-server or production.
  317. pathname: normalizeUrl(
  318. `/${organization.slug}/${project}/events/${dataRow.id}/`
  319. ),
  320. query: {...location.query, referrer: 'discover-events-table'},
  321. };
  322. } else {
  323. if (!dataRow.trace) {
  324. throw new Error(
  325. 'Transaction event should always have a trace associated with it.'
  326. );
  327. }
  328. target = generateLinkToEventInTraceView({
  329. traceSlug: dataRow.trace?.toString(),
  330. eventId: dataRow.id,
  331. projectSlug: (dataRow.project || dataRow['project.name']).toString(),
  332. timestamp: dataRow.timestamp,
  333. organization,
  334. isHomepage,
  335. location,
  336. eventView,
  337. type: 'discover',
  338. source: TraceViewSources.DISCOVER,
  339. });
  340. }
  341. const idLink = (
  342. <StyledLink data-test-id="view-event" to={target}>
  343. {cell}
  344. </StyledLink>
  345. );
  346. cell = (
  347. <QuickContextHoverWrapper
  348. organization={organization}
  349. dataRow={dataRow}
  350. contextType={ContextType.EVENT}
  351. projects={projects}
  352. eventView={eventView}
  353. >
  354. {idLink}
  355. </QuickContextHoverWrapper>
  356. );
  357. } else if (columnKey === 'transaction' && dataRow.transaction) {
  358. cell = (
  359. <TransactionLink
  360. data-test-id="tableView-transaction-link"
  361. to={getTargetForTransactionSummaryLink(
  362. dataRow,
  363. organization,
  364. projects,
  365. eventView,
  366. location
  367. )}
  368. >
  369. {cell}
  370. </TransactionLink>
  371. );
  372. } else if (columnKey === 'trace') {
  373. const timestamp = getTimeStampFromTableDateField(
  374. dataRow['max(timestamp)'] ?? dataRow.timestamp
  375. );
  376. const dateSelection = eventView.normalizeDateSelection(location);
  377. if (dataRow.trace) {
  378. const target = getTraceDetailsUrl({
  379. organization,
  380. traceSlug: String(dataRow.trace),
  381. dateSelection,
  382. timestamp,
  383. location,
  384. source: TraceViewSources.DISCOVER,
  385. });
  386. cell = (
  387. <Tooltip title={t('View Trace')}>
  388. <StyledLink data-test-id="view-trace" to={target}>
  389. {cell}
  390. </StyledLink>
  391. </Tooltip>
  392. );
  393. }
  394. } else if (columnKey === 'replayId') {
  395. if (dataRow.replayId) {
  396. if (!dataRow['project.name']) {
  397. return getShortEventId(String(dataRow.replayId));
  398. }
  399. const target = replayLinkGenerator(organization, dataRow, undefined);
  400. cell = (
  401. <ViewReplayLink replayId={dataRow.replayId} to={target}>
  402. {cell}
  403. </ViewReplayLink>
  404. );
  405. }
  406. } else if (columnKey === 'profile.id') {
  407. const projectSlug = dataRow.project || dataRow['project.name'];
  408. const profileId = dataRow['profile.id'];
  409. if (projectSlug && profileId) {
  410. const target = generateProfileFlamechartRoute({
  411. orgSlug: organization.slug,
  412. projectSlug: String(projectSlug),
  413. profileId: String(profileId),
  414. });
  415. cell = (
  416. <StyledTooltip title={t('View Profile')}>
  417. <StyledLink
  418. data-test-id="view-profile"
  419. to={target}
  420. onClick={() =>
  421. trackAnalytics('profiling_views.go_to_flamegraph', {
  422. organization,
  423. source: 'discover.table',
  424. })
  425. }
  426. >
  427. {cell}
  428. </StyledLink>
  429. </StyledTooltip>
  430. );
  431. }
  432. }
  433. const topResultsIndicator =
  434. isFirstPage && isTopEvents && rowIndex < topEvents && columnIndex === 0 ? (
  435. // Add one if we need to include Other in the series
  436. <TopResultsIndicator count={count} index={rowIndex} />
  437. ) : null;
  438. const fieldName = columnKey;
  439. const value = dataRow[fieldName];
  440. if (
  441. tableData.meta[fieldName] === 'integer' &&
  442. typeof value === 'number' &&
  443. value > 999
  444. ) {
  445. return (
  446. <Tooltip
  447. title={value.toLocaleString()}
  448. containerDisplayMode="block"
  449. position="right"
  450. >
  451. {topResultsIndicator}
  452. <CellAction
  453. column={column}
  454. dataRow={dataRow}
  455. handleCellAction={handleCellAction(dataRow, column)}
  456. >
  457. {cell}
  458. </CellAction>
  459. </Tooltip>
  460. );
  461. }
  462. return (
  463. <Fragment>
  464. {topResultsIndicator}
  465. <CellAction
  466. column={column}
  467. dataRow={dataRow}
  468. handleCellAction={handleCellAction(dataRow, column)}
  469. >
  470. {cell}
  471. </CellAction>
  472. </Fragment>
  473. );
  474. }
  475. function handleEditColumns() {
  476. const {
  477. organization,
  478. eventView,
  479. measurementKeys,
  480. spanOperationBreakdownKeys,
  481. customMeasurements,
  482. dataset,
  483. } = props;
  484. openModal(
  485. modalProps => (
  486. <ColumnEditModal
  487. {...modalProps}
  488. organization={organization}
  489. measurementKeys={measurementKeys}
  490. spanOperationBreakdownKeys={spanOperationBreakdownKeys}
  491. columns={eventView.getColumns().map(col => col.column)}
  492. onApply={handleUpdateColumns}
  493. customMeasurements={customMeasurements}
  494. dataset={dataset}
  495. />
  496. ),
  497. {modalCss, closeEvents: 'escape-key'}
  498. );
  499. }
  500. function handleCellAction(
  501. dataRow: TableDataRow,
  502. column: TableColumn<keyof TableDataRow>
  503. ) {
  504. return (action: Actions, value: React.ReactText) => {
  505. const {eventView, organization, location, tableData, isHomepage, queryDataset} =
  506. props;
  507. const query = new MutableSearch(eventView.query);
  508. let nextView = eventView.clone();
  509. trackAnalytics('discover_v2.results.cellaction', {
  510. organization,
  511. action,
  512. });
  513. switch (action) {
  514. case Actions.RELEASE: {
  515. const maybeProject = projects.find(project => {
  516. return project.slug === dataRow.project;
  517. });
  518. browserHistory.push(
  519. normalizeUrl({
  520. pathname: `/organizations/${
  521. organization.slug
  522. }/releases/${encodeURIComponent(value)}/`,
  523. query: {
  524. ...nextView.getPageFiltersQuery(),
  525. project: maybeProject ? maybeProject.id : undefined,
  526. },
  527. })
  528. );
  529. return;
  530. }
  531. case Actions.DRILLDOWN: {
  532. // count_unique(column) drilldown
  533. trackAnalytics('discover_v2.results.drilldown', {
  534. organization,
  535. });
  536. // Drilldown into each distinct value and get a count() for each value.
  537. nextView = getExpandedResults(nextView, {}, dataRow).withNewColumn({
  538. kind: 'function',
  539. function: ['count', '', undefined, undefined],
  540. });
  541. browserHistory.push(
  542. normalizeUrl(
  543. nextView.getResultsViewUrlTarget(
  544. organization.slug,
  545. isHomepage,
  546. hasDatasetSelector(organization) ? queryDataset : undefined
  547. )
  548. )
  549. );
  550. return;
  551. }
  552. default: {
  553. // Some custom perf metrics have units.
  554. // These custom perf metrics need to be adjusted to the correct value.
  555. let cellValue = value;
  556. const unit = tableData?.meta?.units?.[column.name];
  557. if (typeof cellValue === 'number' && unit) {
  558. if (Object.keys(SIZE_UNITS).includes(unit)) {
  559. cellValue *= SIZE_UNITS[unit];
  560. } else if (Object.keys(DURATION_UNITS).includes(unit)) {
  561. cellValue *= DURATION_UNITS[unit];
  562. }
  563. }
  564. updateQuery(query, action, column, cellValue);
  565. }
  566. }
  567. nextView.query = query.formatString();
  568. const target = nextView.getResultsViewUrlTarget(
  569. organization.slug,
  570. isHomepage,
  571. hasDatasetSelector(organization) ? queryDataset : undefined
  572. );
  573. // Get yAxis from location
  574. target.query.yAxis = decodeList(location.query.yAxis);
  575. browserHistory.push(normalizeUrl(target));
  576. };
  577. }
  578. function handleUpdateColumns(columns: Column[]): void {
  579. const {organization, eventView, location, isHomepage, queryDataset} = props;
  580. // metrics
  581. trackAnalytics('discover_v2.update_columns', {
  582. organization,
  583. });
  584. const nextView = eventView.withColumns(columns);
  585. const resultsViewUrlTarget = nextView.getResultsViewUrlTarget(
  586. organization.slug,
  587. isHomepage,
  588. hasDatasetSelector(organization) ? queryDataset : undefined
  589. );
  590. // Need to pull yAxis from location since eventView only stores 1 yAxis field at time
  591. const previousYAxis = decodeList(location.query.yAxis);
  592. resultsViewUrlTarget.query.yAxis = previousYAxis.filter(yAxis =>
  593. nextView.getYAxisOptions().find(({value}) => value === yAxis)
  594. );
  595. browserHistory.push(normalizeUrl(resultsViewUrlTarget));
  596. }
  597. function renderHeaderButtons() {
  598. const {
  599. organization,
  600. title,
  601. eventView,
  602. isLoading,
  603. error,
  604. tableData,
  605. location,
  606. onChangeShowTags,
  607. showTags,
  608. } = props;
  609. return (
  610. <TableActions
  611. title={title}
  612. isLoading={isLoading}
  613. error={error}
  614. organization={organization}
  615. eventView={eventView}
  616. onEdit={handleEditColumns}
  617. tableData={tableData}
  618. location={location}
  619. onChangeShowTags={onChangeShowTags}
  620. showTags={showTags}
  621. supportsInvestigationRule
  622. />
  623. );
  624. }
  625. const {error, eventView, isLoading, tableData} = props;
  626. const columnOrder = eventView.getColumns();
  627. const columnSortBy = eventView.getSorts();
  628. const prependColumnWidths = eventView.hasAggregateField()
  629. ? ['40px']
  630. : eventView.hasIdField()
  631. ? []
  632. : [`minmax(${COL_WIDTH_MINIMUM}px, max-content)`];
  633. return (
  634. <GridEditable
  635. isLoading={isLoading}
  636. error={error}
  637. data={tableData ? tableData.data : []}
  638. columnOrder={columnOrder}
  639. columnSortBy={columnSortBy}
  640. title={t('Results')}
  641. grid={{
  642. renderHeadCell: _renderGridHeaderCell as any,
  643. renderBodyCell: _renderGridBodyCell as any,
  644. onResizeColumn: _resizeColumn as any,
  645. renderPrependColumns: _renderPrependColumns as any,
  646. prependColumnWidths,
  647. }}
  648. headerButtons={renderHeaderButtons}
  649. />
  650. );
  651. }
  652. const PrependHeader = styled('span')`
  653. color: ${p => p.theme.subText};
  654. `;
  655. const StyledTooltip = styled(Tooltip)`
  656. display: initial;
  657. max-width: max-content;
  658. `;
  659. export const StyledLink = styled(Link)`
  660. & div {
  661. display: inline;
  662. }
  663. `;
  664. export const TransactionLink = styled(Link)`
  665. ${p => p.theme.overflowEllipsis}
  666. `;
  667. const StyledIcon = styled(IconStack)`
  668. vertical-align: middle;
  669. `;
  670. export default TableView;