panelTable.tsx 4.5 KB

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