planTable.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import type {ReactNode} from 'react';
  2. import {Fragment} from 'react';
  3. import styled from '@emotion/styled';
  4. import {IconArrow} from 'sentry/icons';
  5. import {t, tct} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import {DataCategory} from 'sentry/types/core';
  8. import type {Organization} from 'sentry/types/organization';
  9. import {getFormattedDate} from 'sentry/utils/dates';
  10. import type {PreviewData, Subscription} from 'getsentry/types';
  11. import {formatReservedWithUnits} from 'getsentry/utils/billing';
  12. import {displayPriceWithCents} from 'getsentry/views/amCheckout/utils';
  13. import type {Reservations} from './types';
  14. type Props = {
  15. organization: Organization;
  16. previewData: PreviewData;
  17. reservations: Reservations;
  18. subscription: Subscription;
  19. };
  20. function PlanTable({organization, previewData, reservations, subscription}: Props) {
  21. const hasBillingAccess = organization.access?.includes('org:billing');
  22. const planName = subscription.planDetails.name;
  23. const abbr = {isAbbreviated: true};
  24. const {billedAmount, creditApplied, effectiveAt} = previewData;
  25. const effectiveNow = new Date(effectiveAt).getTime() <= new Date().getTime() + 3600;
  26. return (
  27. <Fragment>
  28. <Wrapper>
  29. <TableItem prev={planName} next={planName}>
  30. {t('Plan Type')}
  31. </TableItem>
  32. <TableItem
  33. prev={formatReservedWithUnits(
  34. subscription.categories.errors?.reserved ?? null,
  35. DataCategory.ERRORS,
  36. abbr
  37. )}
  38. next={formatReservedWithUnits(
  39. reservations.reservedErrors,
  40. DataCategory.ERRORS,
  41. abbr
  42. )}
  43. >
  44. {t('Errors')}
  45. </TableItem>
  46. <TableItem
  47. prev={formatReservedWithUnits(
  48. subscription.categories.transactions?.reserved ?? null,
  49. DataCategory.TRANSACTIONS,
  50. abbr
  51. )}
  52. next={formatReservedWithUnits(
  53. reservations.reservedTransactions,
  54. DataCategory.TRANSACTIONS,
  55. abbr
  56. )}
  57. >
  58. {t('Performance Units')}
  59. </TableItem>
  60. <TableItem
  61. prev={0}
  62. next={formatReservedWithUnits(500, DataCategory.REPLAYS, abbr)}
  63. >
  64. {t('Replays')}
  65. </TableItem>
  66. <TableItem
  67. prev={formatReservedWithUnits(
  68. subscription.categories.attachments?.reserved ?? null,
  69. DataCategory.ATTACHMENTS,
  70. abbr
  71. )}
  72. next={formatReservedWithUnits(
  73. reservations.reservedAttachments,
  74. DataCategory.ATTACHMENTS,
  75. abbr
  76. )}
  77. >
  78. {t('Attachments')}
  79. </TableItem>
  80. {billedAmount === 0 ? null : (
  81. <TableItem
  82. isTotal
  83. prev={displayPriceWithCents({
  84. cents: billedAmount === 0 ? 0 : subscription.planDetails.totalPrice,
  85. })}
  86. next={displayPriceWithCents({
  87. cents: subscription.planDetails.totalPrice + billedAmount + creditApplied,
  88. })}
  89. >
  90. {t('Price Change')}
  91. </TableItem>
  92. )}
  93. <TableItem
  94. isTotal
  95. prev={displayPriceWithCents({
  96. cents: billedAmount,
  97. })}
  98. next={displayPriceWithCents({
  99. cents: billedAmount,
  100. })}
  101. >
  102. {t('Total Due')}
  103. </TableItem>
  104. {hasBillingAccess && !effectiveNow ? (
  105. <Fragment>
  106. <PlanLabel isTotal />
  107. <PlanValue isTotal>
  108. <EffectiveDate>
  109. {tct('Effective on [date]', {date: getFormattedDate(effectiveAt, 'll')})}
  110. </EffectiveDate>
  111. </PlanValue>
  112. </Fragment>
  113. ) : null}
  114. </Wrapper>
  115. </Fragment>
  116. );
  117. }
  118. function TableItem({
  119. children,
  120. next,
  121. prev,
  122. isTotal,
  123. }: {
  124. children: ReactNode;
  125. next: string | number;
  126. prev: string | number;
  127. isTotal?: boolean;
  128. }) {
  129. if (prev === next) {
  130. return (
  131. <Fragment>
  132. <PlanLabel isTotal={isTotal}>{children}</PlanLabel>
  133. <PlanValue isTotal={isTotal}>{next}</PlanValue>
  134. </Fragment>
  135. );
  136. }
  137. return (
  138. <Fragment>
  139. <PlanLabel isTotal={isTotal}>{children}</PlanLabel>
  140. <PlanValue isTotal={isTotal}>
  141. {prev}
  142. <IconArrow color="gray300" size="xs" direction="right" />
  143. <strong>{next}</strong>
  144. </PlanValue>
  145. </Fragment>
  146. );
  147. }
  148. const Wrapper = styled('dl')`
  149. display: grid;
  150. grid-template-columns: max-content minmax(max-content, auto);
  151. `;
  152. const PlanLabel = styled('dt')<{hasChanged?: boolean; isTotal?: boolean}>`
  153. padding: ${p => (p.isTotal ? space(1) : `${space(0.5)} ${space(1)}`)};
  154. font-weight: ${p => (p.hasChanged || p.isTotal ? 'bold' : 'normal')};
  155. background: ${p => (p.isTotal ? p.theme.purple100 : 'transparent')};
  156. `;
  157. const PlanValue = styled(PlanLabel)`
  158. text-align: right;
  159. & > svg {
  160. position: relative;
  161. top: 1px;
  162. margin-inline: ${space(0.5)};
  163. }
  164. `;
  165. const EffectiveDate = styled('span')`
  166. font-size: ${p => p.theme.fontSizeExtraSmall};
  167. text-align: right;
  168. `;
  169. export default PlanTable;