index.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import ErrorPanel from 'sentry/components/charts/errorPanel';
  4. import EmptyMessage from 'sentry/components/emptyMessage';
  5. import IdBadge from 'sentry/components/idBadge';
  6. import ExternalLink from 'sentry/components/links/externalLink';
  7. import Link from 'sentry/components/links/link';
  8. import {Panel} from 'sentry/components/panels';
  9. import PanelTable from 'sentry/components/panels/panelTable';
  10. import {IconSettings, IconWarning} from 'sentry/icons';
  11. import {t, tct} from 'sentry/locale';
  12. import space from 'sentry/styles/space';
  13. import {DataCategory, Project} from 'sentry/types';
  14. import theme from 'sentry/utils/theme';
  15. import {formatUsageWithUnits} from '../utils';
  16. const DOCS_URL = 'https://docs.sentry.io/product/accounts/membership/#restricting-access';
  17. type Props = {
  18. dataCategory: DataCategory;
  19. headers: React.ReactNode[];
  20. usageStats: TableStat[];
  21. errors?: Record<string, Error>;
  22. isEmpty?: boolean;
  23. isError?: boolean;
  24. isLoading?: boolean;
  25. };
  26. export type TableStat = {
  27. accepted: number;
  28. dropped: number;
  29. filtered: number;
  30. project: Project;
  31. projectLink: string;
  32. projectSettingsLink: string;
  33. total: number;
  34. };
  35. class UsageTable extends Component<Props> {
  36. get formatUsageOptions() {
  37. const {dataCategory} = this.props;
  38. return {
  39. isAbbreviated: dataCategory !== DataCategory.ATTACHMENTS,
  40. useUnitScaling: dataCategory === DataCategory.ATTACHMENTS,
  41. };
  42. }
  43. getErrorMessage = errorMessage => {
  44. if (errorMessage.projectStats.responseJSON.detail === 'No projects available') {
  45. return (
  46. <EmptyMessage
  47. icon={<IconWarning color="gray300" size="48" />}
  48. title={t(
  49. "You don't have access to any projects, or your organization has no projects."
  50. )}
  51. description={tct('Learn more about [link:Project Access]', {
  52. link: <ExternalLink href={DOCS_URL} />,
  53. })}
  54. />
  55. );
  56. }
  57. return <IconWarning color="gray300" size="48" />;
  58. };
  59. renderTableRow(stat: TableStat & {project: Project}) {
  60. const {dataCategory} = this.props;
  61. const {project, total, accepted, filtered, dropped} = stat;
  62. return [
  63. <CellProject key={0}>
  64. <Link to={stat.projectLink}>
  65. <StyledIdBadge
  66. avatarSize={16}
  67. disableLink
  68. hideOverflow
  69. project={project}
  70. displayName={project.slug}
  71. />
  72. </Link>
  73. <SettingsIconLink to={stat.projectSettingsLink}>
  74. <IconSettings size={theme.iconSizes.sm} />
  75. </SettingsIconLink>
  76. </CellProject>,
  77. <CellStat key={1}>
  78. {formatUsageWithUnits(total, dataCategory, this.formatUsageOptions)}
  79. </CellStat>,
  80. <CellStat key={2}>
  81. {formatUsageWithUnits(accepted, dataCategory, this.formatUsageOptions)}
  82. </CellStat>,
  83. <CellStat key={3}>
  84. {formatUsageWithUnits(filtered, dataCategory, this.formatUsageOptions)}
  85. </CellStat>,
  86. <CellStat key={4}>
  87. {formatUsageWithUnits(dropped, dataCategory, this.formatUsageOptions)}
  88. </CellStat>,
  89. ];
  90. }
  91. render() {
  92. const {isEmpty, isLoading, isError, errors, headers, usageStats} = this.props;
  93. if (isError) {
  94. return (
  95. <Panel>
  96. <ErrorPanel height="256px">{this.getErrorMessage(errors)}</ErrorPanel>
  97. </Panel>
  98. );
  99. }
  100. return (
  101. <StyledPanelTable isLoading={isLoading} isEmpty={isEmpty} headers={headers}>
  102. {usageStats.map(s => this.renderTableRow(s))}
  103. </StyledPanelTable>
  104. );
  105. }
  106. }
  107. export default UsageTable;
  108. const StyledPanelTable = styled(PanelTable)`
  109. grid-template-columns: repeat(5, auto);
  110. @media (min-width: ${p => p.theme.breakpoints.small}) {
  111. grid-template-columns: 1fr repeat(4, minmax(0, auto));
  112. }
  113. `;
  114. export const CellStat = styled('div')`
  115. flex-shrink: 1;
  116. text-align: right;
  117. font-variant-numeric: tabular-nums;
  118. `;
  119. export const CellProject = styled(CellStat)`
  120. display: flex;
  121. align-items: center;
  122. text-align: left;
  123. `;
  124. const StyledIdBadge = styled(IdBadge)`
  125. overflow: hidden;
  126. white-space: nowrap;
  127. flex-shrink: 1;
  128. `;
  129. const SettingsIconLink = styled(Link)`
  130. color: ${p => p.theme.gray300};
  131. align-items: center;
  132. display: inline-flex;
  133. justify-content: space-between;
  134. margin-right: ${space(1.5)};
  135. margin-left: ${space(1.0)};
  136. transition: 0.5s opacity ease-out;
  137. &:hover {
  138. color: ${p => p.theme.textColor};
  139. }
  140. `;