index.tsx 4.3 KB

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