selectorTable.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import {ReactNode, useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import renderSortableHeaderCell from 'sentry/components/feedback/table/renderSortableHeaderCell';
  5. import useQueryBasedColumnResize from 'sentry/components/feedback/table/useQueryBasedColumnResize';
  6. import useQueryBasedSorting from 'sentry/components/feedback/table/useQueryBasedSorting';
  7. import GridEditable, {GridColumnOrder} from 'sentry/components/gridEditable';
  8. import Link from 'sentry/components/links/link';
  9. import TextOverflow from 'sentry/components/textOverflow';
  10. import {Organization} from 'sentry/types';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  13. import {DeadRageSelectorItem} from 'sentry/views/replays/types';
  14. export interface UrlState {
  15. widths: string[];
  16. }
  17. export function getAriaLabel(str: string) {
  18. const pre = str.split('aria="')[1];
  19. if (!pre) {
  20. return '';
  21. }
  22. return pre.substring(0, pre.lastIndexOf('"]'));
  23. }
  24. export function hydratedSelectorData(data, clickType): DeadRageSelectorItem[] {
  25. return data
  26. .filter(d => d[clickType] > 0)
  27. .map(d => {
  28. return {
  29. [clickType]: d[clickType],
  30. dom_element: d.dom_element,
  31. element: d.dom_element.split(/[#.]+/)[0],
  32. aria_label: getAriaLabel(d.dom_element),
  33. };
  34. });
  35. }
  36. interface Props {
  37. clickCountColumn: {key: string; name: string};
  38. data: DeadRageSelectorItem[];
  39. isError: boolean;
  40. isLoading: boolean;
  41. location: Location<any>;
  42. customHandleResize?: () => void;
  43. headerButtons?: ReactNode;
  44. title?: ReactNode;
  45. }
  46. const BASE_COLUMNS: GridColumnOrder<string>[] = [
  47. {key: 'element', name: 'element'},
  48. {key: 'dom_element', name: 'selector'},
  49. {key: 'aria_label', name: 'aria label'},
  50. ];
  51. export default function SelectorTable({
  52. clickCountColumn,
  53. data,
  54. isError,
  55. isLoading,
  56. location,
  57. title,
  58. headerButtons,
  59. customHandleResize,
  60. }: Props) {
  61. const organization = useOrganization();
  62. const {currentSort, makeSortLinkGenerator} = useQueryBasedSorting({
  63. defaultSort: {field: clickCountColumn.key, kind: 'desc'},
  64. location,
  65. });
  66. const {columns, handleResizeColumn} = useQueryBasedColumnResize({
  67. columns: BASE_COLUMNS.concat(clickCountColumn),
  68. location,
  69. });
  70. const renderHeadCell = useMemo(
  71. () =>
  72. renderSortableHeaderCell({
  73. currentSort,
  74. makeSortLinkGenerator,
  75. onClick: () => {},
  76. rightAlignedColumns: [],
  77. sortableColumns: [],
  78. }),
  79. [currentSort, makeSortLinkGenerator]
  80. );
  81. const renderBodyCell = useCallback(
  82. (column, dataRow) => {
  83. const value = dataRow[column.key];
  84. switch (column.key) {
  85. case 'dom_element':
  86. return <SelectorLink organization={organization} value={value} />;
  87. case 'element':
  88. case 'aria_label':
  89. return (
  90. <code>
  91. <TextOverflow>{value}</TextOverflow>
  92. </code>
  93. );
  94. default:
  95. return renderSimpleBodyCell<DeadRageSelectorItem>(column, dataRow);
  96. }
  97. },
  98. [organization]
  99. );
  100. return (
  101. <GridEditable
  102. error={isError}
  103. isLoading={isLoading}
  104. data={data ?? []}
  105. columnOrder={columns}
  106. columnSortBy={[]}
  107. stickyHeader
  108. grid={{
  109. onResizeColumn: customHandleResize ?? handleResizeColumn,
  110. renderHeadCell,
  111. renderBodyCell,
  112. }}
  113. location={location as Location<any>}
  114. title={title}
  115. headerButtons={() => headerButtons}
  116. />
  117. );
  118. }
  119. function SelectorLink({
  120. organization,
  121. value,
  122. }: {
  123. organization: Organization;
  124. value: string;
  125. }) {
  126. return (
  127. <Link
  128. to={{
  129. pathname: normalizeUrl(`/organizations/${organization.slug}/replays/`),
  130. }}
  131. >
  132. <TextOverflow>{value}</TextOverflow>
  133. </Link>
  134. );
  135. }
  136. function renderSimpleBodyCell<T>(column: GridColumnOrder<string>, dataRow: T) {
  137. if (column.key === 'count_dead_clicks') {
  138. return <DeadClickCount>{dataRow[column.key]}</DeadClickCount>;
  139. }
  140. if (column.key === 'count_rage_clicks') {
  141. return <RageClickCount>{dataRow[column.key]}</RageClickCount>;
  142. }
  143. return <TextOverflow>{dataRow[column.key]}</TextOverflow>;
  144. }
  145. const DeadClickCount = styled(TextOverflow)`
  146. color: ${p => p.theme.yellow300};
  147. `;
  148. const RageClickCount = styled(TextOverflow)`
  149. color: ${p => p.theme.red300};
  150. `;