table.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import React, {useCallback, useEffect, useMemo, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import {COL_WIDTH_MINIMUM} from 'sentry/components/gridEditable';
  4. import type {Alignments} from 'sentry/components/gridEditable/sortLink';
  5. import {
  6. Body as _TableWrapper,
  7. Grid as _Table,
  8. GridBody,
  9. GridBodyCell,
  10. GridBodyCellStatus,
  11. GridHead,
  12. GridHeadCell,
  13. GridRow,
  14. Header,
  15. HeaderButtonContainer,
  16. HeaderTitle,
  17. } from 'sentry/components/gridEditable/styles';
  18. import {space} from 'sentry/styles/space';
  19. import {defined} from 'sentry/utils';
  20. import {Actions} from 'sentry/views/discover/table/cellAction';
  21. interface TableProps extends React.ComponentProps<typeof _TableWrapper> {}
  22. export const Table = React.forwardRef<HTMLTableElement, TableProps>(
  23. ({children, styles, ...props}, ref) => (
  24. <_TableWrapper {...props}>
  25. <_Table ref={ref} style={styles}>
  26. {children}
  27. </_Table>
  28. </_TableWrapper>
  29. )
  30. );
  31. interface TableStatusProps {
  32. children: React.ReactNode;
  33. }
  34. export function TableStatus({children}: TableStatusProps) {
  35. return (
  36. <GridRow>
  37. <GridBodyCellStatus>{children}</GridBodyCellStatus>
  38. </GridRow>
  39. );
  40. }
  41. export const ALLOWED_CELL_ACTIONS: Actions[] = [
  42. Actions.ADD,
  43. Actions.EXCLUDE,
  44. Actions.SHOW_GREATER_THAN,
  45. Actions.SHOW_LESS_THAN,
  46. ];
  47. const MINIMUM_COLUMN_WIDTH = COL_WIDTH_MINIMUM;
  48. export function useTableStyles(
  49. fields: string[],
  50. tableRef: React.RefObject<HTMLDivElement>,
  51. options?: {
  52. minimumColumnWidth?: number;
  53. prefixColumnWidth?: 'min-content' | number;
  54. }
  55. ) {
  56. const minimumColumnWidth = options?.minimumColumnWidth ?? MINIMUM_COLUMN_WIDTH;
  57. const prefixColumnWidth =
  58. defined(options?.prefixColumnWidth) && typeof options.prefixColumnWidth === 'number'
  59. ? `${options.prefixColumnWidth}px`
  60. : options?.prefixColumnWidth;
  61. const resizingColumnIndex = useRef<number | null>(null);
  62. const columnWidthsRef = useRef<Array<number | null>>(fields.map(() => null));
  63. useEffect(() => {
  64. columnWidthsRef.current = fields.map(
  65. (_, index) => columnWidthsRef.current[index] ?? null
  66. );
  67. }, [fields]);
  68. const initialTableStyles = useMemo(() => {
  69. const gridTemplateColumns = fields.map(() => `minmax(${minimumColumnWidth}px, auto)`);
  70. if (defined(prefixColumnWidth)) {
  71. gridTemplateColumns.unshift(prefixColumnWidth);
  72. }
  73. return {
  74. gridTemplateColumns: gridTemplateColumns.join(' '),
  75. };
  76. }, [fields, minimumColumnWidth, prefixColumnWidth]);
  77. const onResizeMouseDown = useCallback(
  78. (event: React.MouseEvent<HTMLDivElement>, index: number) => {
  79. event.preventDefault();
  80. // <GridResizer> is expected to be nested 1 level down from <GridHeadCell>
  81. const cell = event.currentTarget.parentElement;
  82. if (!cell) {
  83. return;
  84. }
  85. resizingColumnIndex.current = index;
  86. const startX = event.clientX;
  87. const initialWidth = cell.offsetWidth;
  88. const gridElement = tableRef.current;
  89. function onMouseMove(e: MouseEvent) {
  90. if (resizingColumnIndex.current === null || !gridElement) {
  91. return;
  92. }
  93. const newWidth = Math.max(
  94. minimumColumnWidth,
  95. initialWidth + (e.clientX - startX)
  96. );
  97. columnWidthsRef.current[index] = newWidth;
  98. // Updating the grid's `gridTemplateColumns` directly
  99. const gridTemplateColumns = columnWidthsRef.current.map(width => {
  100. return typeof width === 'number'
  101. ? `${width}px`
  102. : `minmax(${minimumColumnWidth}px, auto)`;
  103. });
  104. if (defined(prefixColumnWidth)) {
  105. gridTemplateColumns.unshift(prefixColumnWidth);
  106. }
  107. gridElement.style.gridTemplateColumns = gridTemplateColumns.join(' ');
  108. }
  109. function onMouseUp() {
  110. resizingColumnIndex.current = null;
  111. // Cleaning up event listeners
  112. window.removeEventListener('mousemove', onMouseMove);
  113. window.removeEventListener('mouseup', onMouseUp);
  114. }
  115. window.addEventListener('mousemove', onMouseMove);
  116. window.addEventListener('mouseup', onMouseUp);
  117. },
  118. [tableRef, minimumColumnWidth, prefixColumnWidth]
  119. );
  120. return {initialTableStyles, onResizeMouseDown};
  121. }
  122. export const TableBody = GridBody;
  123. export const TableRow = GridRow;
  124. export const TableBodyCell = GridBodyCell;
  125. export const TableHead = GridHead;
  126. export const TableHeader = Header;
  127. export const TableHeaderActions = HeaderButtonContainer;
  128. export const TableHeaderTitle = HeaderTitle;
  129. export const TableHeadCell = styled(GridHeadCell)<{align?: Alignments}>`
  130. ${p => p.align && `justify-content: ${p.align};`}
  131. `;
  132. export const TableHeadCellContent = styled('div')`
  133. display: flex;
  134. align-items: center;
  135. gap: ${space(0.5)};
  136. cursor: pointer;
  137. `;