selectorTable.tsx 4.8 KB

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