rootAllocationCard.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import {useMemo} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import capitalize from 'lodash/capitalize';
  5. import {Button} from 'sentry/components/button';
  6. import ExternalLink from 'sentry/components/links/externalLink';
  7. import {Tooltip} from 'sentry/components/tooltip';
  8. import {IconAdd} from 'sentry/icons';
  9. import {t, tct} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import {DataCategory} from 'sentry/types/core';
  12. import {PlanTier, type Subscription} from 'getsentry/types';
  13. import {displayPrice} from 'getsentry/views/amCheckout/utils';
  14. import {Card, HalvedGrid} from './components/styles';
  15. import type {SpendAllocation} from './components/types';
  16. import {bigNumFormatter, BigNumUnits} from './utils';
  17. type Props = {
  18. createRootAllocation: (e: React.MouseEvent) => void;
  19. selectedMetric: string;
  20. subscription: Subscription;
  21. rootAllocation?: SpendAllocation;
  22. };
  23. function RootAllocationCard({
  24. createRootAllocation,
  25. rootAllocation,
  26. selectedMetric,
  27. subscription,
  28. }: Props) {
  29. const theme = useTheme();
  30. const availableEvents = useMemo(() => {
  31. return rootAllocation
  32. ? Math.max(rootAllocation.reservedQuantity - rootAllocation.consumedQuantity, 0)
  33. : 0;
  34. }, [rootAllocation]);
  35. const metricUnit = useMemo(() => {
  36. return selectedMetric === DataCategory.ATTACHMENTS
  37. ? BigNumUnits.KILO_BYTES
  38. : BigNumUnits.NUMBERS;
  39. }, [selectedMetric]);
  40. return (
  41. <RootAllocation>
  42. {!rootAllocation && (
  43. <Card data-test-id="missing-root">
  44. <CreateRoot>
  45. <NoRootInfo>
  46. There is currently no organization-level allocation for this billing metric.
  47. <p>
  48. An organization-level allocation is required to distribute allocations to
  49. projects.
  50. </p>
  51. Click the button to create one and to enable spend allocation for{' '}
  52. {selectedMetric}.
  53. </NoRootInfo>
  54. <EnableRoot>
  55. <Button
  56. icon={<IconAdd />}
  57. onClick={createRootAllocation}
  58. disabled={rootAllocation}
  59. >
  60. Create Organization-Level Allocation
  61. </Button>
  62. </EnableRoot>
  63. </CreateRoot>
  64. </Card>
  65. )}
  66. {rootAllocation && (
  67. <Card>
  68. <HalvedGrid>
  69. <div>
  70. <Header>
  71. {t('Un-Allocated ')}
  72. {capitalize(selectedMetric)}&nbsp;
  73. {t('Pool')}
  74. </Header>
  75. <Body>
  76. {tct(
  77. `The un-allocated pool represents the remaining Reserved Volume available for your projects. Excess project consumption will first consume events from your un-allocated pool, and then from your [odLink] volume, if available`,
  78. {
  79. odLink: (
  80. <ExternalLink href="https://docs.sentry.io/product/accounts/pricing/#on-demand-capacity">
  81. {subscription.planTier === PlanTier.AM3
  82. ? 'Pay-as-you-go'
  83. : 'On-Demand'}
  84. </ExternalLink>
  85. ),
  86. }
  87. )}
  88. </Body>
  89. </div>
  90. <Table>
  91. <colgroup>
  92. <col style={{width: '50%'}} />
  93. <col />
  94. <col />
  95. </colgroup>
  96. <tbody>
  97. <tr>
  98. <THead />
  99. <THead>$ Spend</THead>
  100. <THead>Event Volume</THead>
  101. </tr>
  102. <tr>
  103. <Cell>Available</Cell>
  104. <Cell>
  105. {rootAllocation.costPerItem === 0 ? (
  106. <Tooltip title="Cost per event is unavailable for base plans">
  107. --
  108. </Tooltip>
  109. ) : (
  110. displayPrice({
  111. cents: rootAllocation.costPerItem * availableEvents,
  112. })
  113. )}
  114. </Cell>
  115. <Cell>
  116. <Tooltip title={availableEvents.toLocaleString()}>
  117. {bigNumFormatter(availableEvents, 2, metricUnit)}
  118. </Tooltip>
  119. </Cell>
  120. </tr>
  121. <tr>
  122. <Cell>Consumed</Cell>
  123. <Cell>
  124. {/* TODO: include OD costs if enabled */}
  125. {rootAllocation.costPerItem === 0 ? (
  126. <Tooltip title="Cost per event is unavailable for base plans">
  127. --
  128. </Tooltip>
  129. ) : (
  130. displayPrice({
  131. cents:
  132. rootAllocation.costPerItem *
  133. Math.min(
  134. rootAllocation.reservedQuantity,
  135. rootAllocation.consumedQuantity
  136. ),
  137. })
  138. )}
  139. </Cell>
  140. <Cell>
  141. <Tooltip title={rootAllocation.consumedQuantity.toLocaleString()}>
  142. {bigNumFormatter(
  143. Math.min(
  144. rootAllocation.reservedQuantity,
  145. rootAllocation.consumedQuantity
  146. ),
  147. 2,
  148. metricUnit
  149. )}
  150. </Tooltip>
  151. {rootAllocation.consumedQuantity >
  152. rootAllocation.reservedQuantity && (
  153. <Tooltip
  154. title={
  155. rootAllocation.consumedQuantity -
  156. rootAllocation.reservedQuantity
  157. }
  158. >
  159. &nbsp;
  160. <span style={{color: theme.red400, marginLeft: space(1)}}>
  161. (
  162. {bigNumFormatter(
  163. rootAllocation.consumedQuantity -
  164. rootAllocation.reservedQuantity,
  165. 2,
  166. metricUnit
  167. )}{' '}
  168. over)
  169. </span>
  170. </Tooltip>
  171. )}
  172. </Cell>
  173. </tr>
  174. </tbody>
  175. </Table>
  176. </HalvedGrid>
  177. </Card>
  178. )}
  179. </RootAllocation>
  180. );
  181. }
  182. export default RootAllocationCard;
  183. const Header = styled('div')`
  184. display: flex;
  185. color: ${p => p.theme.gray400};
  186. font-weight: 600;
  187. font-size: ${p => p.theme.fontSizeLarge};
  188. padding: ${space(1)};
  189. `;
  190. const Body = styled('div')`
  191. padding: ${space(1)};
  192. `;
  193. const RootAllocation = styled('div')`
  194. margin: ${space(2)} 0;
  195. `;
  196. const CreateRoot = styled('div')`
  197. display: flex;
  198. justify-content: space-between;
  199. `;
  200. const EnableRoot = styled('div')`
  201. grid-column: -auto / span 1;
  202. grid-area: bt;
  203. display: flex;
  204. justify-content: center;
  205. align-items: center;
  206. `;
  207. const NoRootInfo = styled('div')`
  208. margin-right: ${space(2)};
  209. `;
  210. const Table = styled('table')`
  211. tr:nth-child(even) {
  212. background-color: ${p => p.theme.bodyBackground};
  213. }
  214. `;
  215. const THead = styled('th')`
  216. padding: ${space(1)};
  217. `;
  218. const Cell = styled('td')`
  219. padding: ${space(1)};
  220. `;