panelTable.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import * as React 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. export 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 = ({
  64. headers,
  65. children,
  66. isLoading,
  67. isEmpty,
  68. disablePadding,
  69. className,
  70. emptyMessage = t('There are no items to display'),
  71. emptyAction,
  72. loader,
  73. stickyHeaders = false,
  74. ...props
  75. }: PanelTableProps) => {
  76. const shouldShowLoading = isLoading === true;
  77. const shouldShowEmptyMessage = !shouldShowLoading && isEmpty;
  78. const shouldShowContent = !shouldShowLoading && !shouldShowEmptyMessage;
  79. return (
  80. <Wrapper
  81. columns={headers.length}
  82. disablePadding={disablePadding}
  83. className={className}
  84. hasRows={shouldShowContent}
  85. {...props}
  86. >
  87. {headers.map((header, i) => (
  88. <PanelTableHeader key={i} sticky={stickyHeaders}>
  89. {header}
  90. </PanelTableHeader>
  91. ))}
  92. {shouldShowLoading && (
  93. <LoadingWrapper>{loader || <LoadingIndicator />}</LoadingWrapper>
  94. )}
  95. {shouldShowEmptyMessage && (
  96. <TableEmptyStateWarning>
  97. <p>{emptyMessage}</p>
  98. {emptyAction}
  99. </TableEmptyStateWarning>
  100. )}
  101. {shouldShowContent && getContent(children)}
  102. </Wrapper>
  103. );
  104. };
  105. function getContent(children: PanelTableProps['children']) {
  106. if (typeof children === 'function') {
  107. return children();
  108. }
  109. return children;
  110. }
  111. type WrapperProps = {
  112. /**
  113. * The number of columns the table will have, this is derived from the headers list
  114. */
  115. columns: number;
  116. disablePadding: PanelTableProps['disablePadding'];
  117. hasRows: boolean;
  118. };
  119. const LoadingWrapper = styled('div')``;
  120. const TableEmptyStateWarning = styled(EmptyStateWarning)``;
  121. const Wrapper = styled(Panel, {
  122. shouldForwardProp: p => typeof p === 'string' && isPropValid(p) && p !== 'columns',
  123. })<WrapperProps>`
  124. display: grid;
  125. grid-template-columns: repeat(${p => p.columns}, auto);
  126. > * {
  127. ${p => (p.disablePadding ? '' : `padding: ${space(2)};`)}
  128. &:nth-last-child(n + ${p => (p.hasRows ? p.columns + 1 : 0)}) {
  129. border-bottom: 1px solid ${p => p.theme.border};
  130. }
  131. }
  132. > ${/* sc-selector */ TableEmptyStateWarning}, > ${/* sc-selector */ LoadingWrapper} {
  133. border: none;
  134. grid-column: auto / span ${p => p.columns};
  135. }
  136. /* safari needs an overflow value or the contents will spill out */
  137. overflow: auto;
  138. `;
  139. export const PanelTableHeader = styled('div')<{sticky: boolean}>`
  140. color: ${p => p.theme.subText};
  141. font-size: ${p => p.theme.fontSizeSmall};
  142. font-weight: 600;
  143. text-transform: uppercase;
  144. border-radius: ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0 0;
  145. background: ${p => p.theme.backgroundSecondary};
  146. line-height: 1;
  147. display: flex;
  148. flex-direction: column;
  149. justify-content: center;
  150. min-height: 45px;
  151. ${p =>
  152. p.sticky &&
  153. `
  154. position: sticky;
  155. top: 0;
  156. z-index: ${p.theme.zIndex.initial};
  157. `}
  158. `;
  159. export default PanelTable;