planList.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import SelectField from 'sentry/components/forms/fields/selectField';
  4. import {space} from 'sentry/styles/space';
  5. import {DataCategory} from 'sentry/types/core';
  6. import type {Plan} from 'getsentry/types';
  7. import {getPlanCategoryName} from 'getsentry/utils/dataCategory';
  8. import formatCurrency from 'getsentry/utils/formatCurrency';
  9. import titleCase from 'getsentry/utils/titleCase';
  10. type LimitName =
  11. | 'reservedErrors'
  12. | 'reservedAttachments'
  13. | 'reservedReplays'
  14. | 'reservedTransactions'
  15. | 'reservedMonitorSeats'
  16. | 'reservedUptime'
  17. | 'reservedSpans'
  18. | 'reservedProfileDuration';
  19. type Props = {
  20. onLimitChange: (limit: LimitName, value: number) => void;
  21. onPlanChange: (planId: string) => void;
  22. planId: null | string;
  23. plans: Plan[];
  24. reservedAttachments: null | number;
  25. reservedErrors: null | number;
  26. reservedMonitorSeats: null | number;
  27. reservedProfileDuration: null | number;
  28. reservedReplays: null | number;
  29. reservedSpans: null | number;
  30. reservedTransactions: null | number;
  31. reservedUptime: null | number;
  32. };
  33. const configurableCategories: DataCategory[] = [
  34. DataCategory.ERRORS,
  35. DataCategory.REPLAYS,
  36. DataCategory.TRANSACTIONS,
  37. DataCategory.ATTACHMENTS,
  38. DataCategory.MONITOR_SEATS,
  39. DataCategory.UPTIME,
  40. DataCategory.SPANS,
  41. DataCategory.PROFILE_DURATION,
  42. ];
  43. function PlanList({
  44. plans,
  45. planId,
  46. reservedErrors,
  47. reservedTransactions,
  48. reservedReplays,
  49. reservedAttachments,
  50. reservedMonitorSeats,
  51. reservedUptime,
  52. reservedProfileDuration,
  53. reservedSpans,
  54. onPlanChange,
  55. onLimitChange,
  56. }: Props) {
  57. const changeValue = {
  58. 6000000: '6M',
  59. 5000000: '5M',
  60. 4000000: '4M',
  61. 3000000: '3M',
  62. 1500000: '1.5M',
  63. 500000: '500k',
  64. 100000: '100K',
  65. };
  66. if (!plans.length) {
  67. return null;
  68. }
  69. function handleLimitChange(limit: LimitName) {
  70. return function handleChange(value: string) {
  71. onLimitChange(limit, parseInt(value, 10));
  72. };
  73. }
  74. const activePlan = plans.find(plan => plan.id === planId);
  75. return (
  76. <Fragment>
  77. {plans.map(plan => (
  78. <div key={plan.id}>
  79. <PlanLabel>
  80. <div>
  81. <input
  82. data-test-id={`change-plan-radio-btn-${plan.id}`}
  83. type="radio"
  84. name="cancelAtPeriodEnd"
  85. value={plan.id}
  86. onChange={() => onPlanChange(plan.id)}
  87. />
  88. </div>
  89. <div>
  90. <strong>
  91. {plan.name}{' '}
  92. {
  93. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  94. changeValue[plan.reservedMinimum] ?? ''
  95. }
  96. </strong>{' '}
  97. <SubText>— {plan.id}</SubText>
  98. <br />
  99. <small>
  100. {formatCurrency(plan.price)} /{' '}
  101. {plan.billingInterval === 'annual' ? 'annually' : 'monthly'}
  102. </small>
  103. </div>
  104. </PlanLabel>
  105. </div>
  106. ))}
  107. {activePlan &&
  108. (
  109. activePlan?.planCategories.transactions ||
  110. activePlan?.planCategories.spans ||
  111. []
  112. ).length > 1 && (
  113. <div>
  114. <h4>Reserved Volumes</h4>
  115. {activePlan.categories
  116. .filter(category =>
  117. configurableCategories.includes(category as DataCategory)
  118. )
  119. .map(category => {
  120. const titleCategory = getPlanCategoryName({plan: activePlan, category});
  121. const reserved = `reserved${
  122. titleCase(category[0]!) + category.substring(1, category.length)
  123. }`;
  124. const label =
  125. category === DataCategory.ATTACHMENTS
  126. ? `${titleCategory} (GB)`
  127. : titleCategory;
  128. let fieldValue: any;
  129. switch (category) {
  130. case DataCategory.ERRORS:
  131. fieldValue = reservedErrors;
  132. break;
  133. case DataCategory.TRANSACTIONS:
  134. fieldValue = reservedTransactions;
  135. break;
  136. case DataCategory.SPANS:
  137. fieldValue = reservedSpans;
  138. break;
  139. case DataCategory.REPLAYS:
  140. fieldValue = reservedReplays;
  141. break;
  142. case DataCategory.ATTACHMENTS:
  143. fieldValue = reservedAttachments;
  144. break;
  145. case DataCategory.MONITOR_SEATS:
  146. fieldValue = reservedMonitorSeats;
  147. break;
  148. case DataCategory.PROFILE_DURATION:
  149. fieldValue = reservedProfileDuration;
  150. break;
  151. case DataCategory.UPTIME:
  152. fieldValue = reservedUptime;
  153. break;
  154. default:
  155. throw new Error(`Category ${category} is not supported`);
  156. }
  157. return (
  158. <SelectField
  159. key={`test-${category}`}
  160. inline={false}
  161. stacked
  162. name={`${reserved}`}
  163. label={label}
  164. value={fieldValue}
  165. options={(activePlan.planCategories[category] || []).map(level => ({
  166. label: level.events.toLocaleString(),
  167. value: level.events,
  168. }))}
  169. required
  170. onChange={handleLimitChange(reserved as LimitName)}
  171. />
  172. );
  173. })}
  174. </div>
  175. )}
  176. </Fragment>
  177. );
  178. }
  179. const PlanLabel = styled('label')`
  180. margin-bottom: 10px;
  181. display: flex;
  182. align-items: flex-start;
  183. & > div {
  184. margin-right: ${space(3)};
  185. }
  186. `;
  187. const SubText = styled('small')`
  188. font-weight: normal;
  189. color: #999;
  190. `;
  191. export default PlanList;