panelTable.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import {forwardRef} from 'react';
  2. import isPropValid from '@emotion/is-prop-valid';
  3. import styled from '@emotion/styled';
  4. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import Panel from './panel';
  9. type PanelTableProps = {
  10. /**
  11. * Headers of the table.
  12. */
  13. headers: React.ReactNode[];
  14. /**
  15. * The body of the table. Make sure the number of children elements are
  16. * multiples of the length of headers.
  17. */
  18. children?: React.ReactNode | (() => React.ReactNode);
  19. className?: string;
  20. /**
  21. * Renders without predefined padding on the header and body cells
  22. */
  23. disablePadding?: boolean;
  24. /**
  25. * Action to display when isEmpty is true
  26. */
  27. emptyAction?: React.ReactNode;
  28. /**
  29. * Message to use for `<EmptyStateWarning>`
  30. */
  31. emptyMessage?: React.ReactNode;
  32. /**
  33. * Displays an `<EmptyStateWarning>` if true
  34. */
  35. isEmpty?: boolean;
  36. /**
  37. * If this is true, then display a loading indicator
  38. */
  39. isLoading?: boolean;
  40. /**
  41. * A custom loading indicator.
  42. */
  43. loader?: React.ReactNode;
  44. /**
  45. * If true, scrolling headers out of view will pin to the top of container.
  46. */
  47. stickyHeaders?: boolean;
  48. };
  49. /**
  50. * Bare bones table generates a CSS grid template based on the content.
  51. *
  52. * The number of children elements should be a multiple of `this.props.columns` to have
  53. * it look ok.
  54. *
  55. *
  56. * Potential customizations:
  57. * - [ ] Add borders for columns to make them more like cells
  58. * - [ ] Add prop to disable borders for rows
  59. * - [ ] We may need to wrap `children` with our own component (similar to what we're doing
  60. * with `headers`. Then we can get rid of that gross `> *` selector
  61. * - [ ] Allow customization of wrappers (Header and body cells if added)
  62. */
  63. const PanelTable = forwardRef<HTMLDivElement, PanelTableProps>(function PanelTable(
  64. {
  65. headers,
  66. children,
  67. isLoading,
  68. isEmpty,
  69. disablePadding,
  70. className,
  71. emptyMessage = t('There are no items to display'),
  72. emptyAction,
  73. loader,
  74. stickyHeaders = false,
  75. ...props
  76. }: PanelTableProps,
  77. ref: React.Ref<HTMLDivElement>
  78. ) {
  79. const shouldShowLoading = isLoading === true;
  80. const shouldShowEmptyMessage = !shouldShowLoading && isEmpty;
  81. const shouldShowContent = !shouldShowLoading && !shouldShowEmptyMessage;
  82. return (
  83. <Wrapper
  84. ref={ref}
  85. columns={headers.length}
  86. disablePadding={disablePadding}
  87. className={className}
  88. hasRows={shouldShowContent}
  89. {...props}
  90. >
  91. {headers.map((header, i) => (
  92. <PanelTableHeader key={i} sticky={stickyHeaders} data-test-id="table-header">
  93. {header}
  94. </PanelTableHeader>
  95. ))}
  96. {shouldShowLoading && (
  97. <LoadingWrapper>{loader || <LoadingIndicator />}</LoadingWrapper>
  98. )}
  99. {shouldShowEmptyMessage && (
  100. <TableEmptyStateWarning>
  101. <p>{emptyMessage}</p>
  102. {emptyAction}
  103. </TableEmptyStateWarning>
  104. )}
  105. {shouldShowContent && getContent(children)}
  106. </Wrapper>
  107. );
  108. });
  109. function getContent(children: PanelTableProps['children']) {
  110. if (typeof children === 'function') {
  111. return children();
  112. }
  113. return children;
  114. }
  115. type WrapperProps = {
  116. /**
  117. * The number of columns the table will have, this is derived from the headers list
  118. */
  119. columns: number;
  120. disablePadding: PanelTableProps['disablePadding'];
  121. hasRows: boolean;
  122. };
  123. const LoadingWrapper = styled('div')``;
  124. const TableEmptyStateWarning = styled(EmptyStateWarning)``;
  125. const Wrapper = styled(Panel, {
  126. shouldForwardProp: p => typeof p === 'string' && isPropValid(p) && p !== 'columns',
  127. })<WrapperProps>`
  128. display: grid;
  129. grid-template-columns: repeat(${p => p.columns}, auto);
  130. > * {
  131. ${p => (p.disablePadding ? '' : `padding: ${space(2)};`)}
  132. &:nth-last-child(n + ${p => (p.hasRows ? p.columns + 1 : 0)}) {
  133. border-bottom: 1px solid ${p => p.theme.border};
  134. }
  135. }
  136. > ${TableEmptyStateWarning}, > ${LoadingWrapper} {
  137. border: none;
  138. grid-column: auto / span ${p => p.columns};
  139. }
  140. /* safari needs an overflow value or the contents will spill out */
  141. overflow: auto;
  142. `;
  143. const PanelTableHeader = styled('div')<{sticky: boolean}>`
  144. color: ${p => p.theme.subText};
  145. font-size: ${p => p.theme.fontSizeSmall};
  146. font-weight: 600;
  147. text-transform: uppercase;
  148. border-radius: ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0 0;
  149. background: ${p => p.theme.backgroundSecondary};
  150. line-height: 1;
  151. display: flex;
  152. flex-direction: column;
  153. justify-content: center;
  154. min-height: 45px;
  155. ${p =>
  156. p.sticky &&
  157. `
  158. position: sticky;
  159. top: 0;
  160. z-index: ${p.theme.zIndex.initial};
  161. `}
  162. `;
  163. export {PanelTable, type PanelTableProps, PanelTableHeader};
  164. export default PanelTable;