customerOverview.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import upperFirst from 'lodash/upperFirst';
  4. import moment from 'moment-timezone';
  5. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  6. import type {ResponseMeta} from 'sentry/api';
  7. import {Button} from 'sentry/components/core/button';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import {Tooltip} from 'sentry/components/tooltip';
  10. import {tct} from 'sentry/locale';
  11. import ConfigStore from 'sentry/stores/configStore';
  12. import {DataCategory} from 'sentry/types/core';
  13. import type {Organization} from 'sentry/types/organization';
  14. import {defined} from 'sentry/utils';
  15. import useApi from 'sentry/utils/useApi';
  16. import ChangeARRAction from 'admin/components/changeARRAction';
  17. import ChangeContractEndDateAction from 'admin/components/changeContractEndDateAction';
  18. import CustomerContact from 'admin/components/customerContact';
  19. import CustomerStatus from 'admin/components/customerStatus';
  20. import DetailLabel from 'admin/components/detailLabel';
  21. import DetailList from 'admin/components/detailList';
  22. import DetailsContainer from 'admin/components/detailsContainer';
  23. import {getLogQuery} from 'admin/utils';
  24. import {PRODUCT_TRIAL_CATEGORIES, UNLIMITED} from 'getsentry/constants';
  25. import type {
  26. Plan,
  27. ReservedBudget,
  28. ReservedBudgetMetricHistory,
  29. Subscription,
  30. } from 'getsentry/types';
  31. import {BillingType, OnDemandBudgetMode} from 'getsentry/types';
  32. import {formatBalance, formatReservedWithUnits} from 'getsentry/utils/billing';
  33. import {
  34. getPlanCategoryName,
  35. getReservedBudgetDisplayName,
  36. sortCategories,
  37. } from 'getsentry/utils/dataCategory';
  38. import formatCurrency from 'getsentry/utils/formatCurrency';
  39. import {getCountryByCode} from 'getsentry/utils/ISO3166codes';
  40. import titleCase from 'getsentry/utils/titleCase';
  41. import {displayPriceWithCents} from 'getsentry/views/amCheckout/utils';
  42. type SubscriptionSummaryProps = {
  43. customer: Subscription;
  44. onAction: (data: any) => void;
  45. };
  46. function SoftCapTypeDetail({
  47. categories,
  48. plan,
  49. }: {
  50. categories: Subscription['categories'];
  51. plan: Plan;
  52. }) {
  53. if (!categories) {
  54. return <span>None</span>;
  55. }
  56. const shouldUseDsNames = plan.categories.includes(DataCategory.SPANS_INDEXED);
  57. const softCapTypes = sortCategories(categories)
  58. .map(categoryHistory => {
  59. const softCapName = categoryHistory.softCapType
  60. ? titleCase(categoryHistory.softCapType.replace(/_/g, ' '))
  61. : null;
  62. if (softCapName) {
  63. return (
  64. <Fragment key={`test-soft-cap-type-${categoryHistory.category}`}>
  65. <small>
  66. {`${getPlanCategoryName({
  67. plan,
  68. category: categoryHistory.category,
  69. capitalize: true,
  70. hadCustomDynamicSampling: shouldUseDsNames,
  71. })}: `}
  72. {`${softCapName}`}
  73. </small>
  74. <br />
  75. </Fragment>
  76. );
  77. }
  78. return null;
  79. })
  80. .filter(i => i);
  81. return <Fragment>{softCapTypes.length ? softCapTypes : <span>None</span>}</Fragment>;
  82. }
  83. function SubscriptionSummary({customer, onAction}: SubscriptionSummaryProps) {
  84. return (
  85. <div>
  86. <DetailList>
  87. <DetailLabel title="Balance">
  88. {formatBalance(customer.accountBalance)}
  89. {customer.type === BillingType.INVOICED && (
  90. <Fragment>
  91. <br />
  92. <small>
  93. Note: this is an invoiced account, please send any questions about this
  94. balance to <a href="mailto:salesops@sentry.io">salesops@sentry.io</a>
  95. </small>
  96. </Fragment>
  97. )}
  98. </DetailLabel>
  99. <DetailLabel title="Billing Period">
  100. {`${moment(customer.billingPeriodStart).format('ll')} › ${moment(
  101. customer.billingPeriodEnd
  102. ).format('ll')}`}
  103. <br />
  104. <small>{customer.billingInterval}</small>
  105. </DetailLabel>
  106. {customer.contractPeriodStart && (
  107. <DetailLabel title="Contract Period">
  108. {`${moment(customer.contractPeriodStart).format('ll')} › `}
  109. {(customer.contractInterval === 'annual' &&
  110. customer.type === BillingType.INVOICED && (
  111. <ChangeContractEndDateAction
  112. contractPeriodEnd={customer.contractPeriodEnd}
  113. onAction={onAction}
  114. />
  115. )) ||
  116. `${moment(customer.contractPeriodEnd).format('ll')}`}
  117. <br />
  118. <small>{customer.contractInterval}</small>
  119. </DetailLabel>
  120. )}
  121. <DetailLabel title="On-Demand">
  122. <OnDemandSummary customer={customer} />
  123. </DetailLabel>
  124. <DetailLabel title="Can Trial" yesNo={customer.canTrial} />
  125. <DetailLabel title="Can Grace Period" yesNo={customer.canGracePeriod} />
  126. <DetailLabel title="Legacy Soft Cap" yesNo={customer.hasSoftCap} />
  127. {customer.hasSoftCap && (
  128. <DetailLabel
  129. title="Overage Notifications Disabled"
  130. yesNo={customer.hasOverageNotificationsDisabled}
  131. />
  132. )}
  133. <DetailLabel title="Soft Cap By Category">
  134. <SoftCapTypeDetail
  135. categories={customer.categories}
  136. plan={customer.planDetails}
  137. />
  138. </DetailLabel>
  139. {defined(customer.msaUpdatedForDataConsent) && (
  140. <DetailLabel
  141. title="MSA Updated for Data Consent"
  142. yesNo={customer.msaUpdatedForDataConsent}
  143. />
  144. )}
  145. </DetailList>
  146. </div>
  147. );
  148. }
  149. type ReservedDataProps = {
  150. customer: Subscription;
  151. };
  152. type ReservedBudgetProps = {
  153. customer: Subscription;
  154. reservedBudget: ReservedBudget;
  155. };
  156. function ReservedData({customer}: ReservedDataProps) {
  157. const reservedBudgetMetricHistories: Record<string, ReservedBudgetMetricHistory> = {};
  158. customer.reservedBudgets?.forEach(budget => {
  159. Object.entries(budget.categories).forEach(([category, history]) => {
  160. reservedBudgetMetricHistories[category] = history;
  161. });
  162. });
  163. return (
  164. <Fragment>
  165. {sortCategories(customer.categories).map(categoryHistory => {
  166. const category = categoryHistory.category;
  167. const categoryName = getPlanCategoryName({
  168. plan: customer.planDetails,
  169. category: categoryHistory.category,
  170. hadCustomDynamicSampling:
  171. category === DataCategory.SPANS &&
  172. DataCategory.SPANS_INDEXED in customer.categories,
  173. });
  174. return (
  175. <Fragment key={category}>
  176. <h6>{categoryName}</h6>
  177. <DetailList>
  178. <DetailLabel title={`Reserved ${categoryName}`}>
  179. {formatReservedWithUnits(categoryHistory.reserved, category)}
  180. </DetailLabel>
  181. {reservedBudgetMetricHistories[category] && (
  182. <Fragment>
  183. <DetailLabel title={`Reserved Cost-Per-Event ${categoryName}`}>
  184. {displayPriceWithCents({
  185. cents: reservedBudgetMetricHistories[category].reservedCpe,
  186. minimumFractionDigits: 8,
  187. maximumFractionDigits: 8,
  188. })}
  189. </DetailLabel>
  190. <DetailLabel title={`Reserved Spend ${categoryName}`}>
  191. {displayPriceWithCents({
  192. cents: reservedBudgetMetricHistories[category].reservedSpend,
  193. minimumFractionDigits: 2,
  194. maximumFractionDigits: 2,
  195. })}
  196. </DetailLabel>
  197. </Fragment>
  198. )}
  199. <DetailLabel title={`Custom Price ${categoryName}`}>
  200. {typeof categoryHistory.customPrice === 'number'
  201. ? displayPriceWithCents({cents: categoryHistory.customPrice})
  202. : 'None'}
  203. </DetailLabel>
  204. {customer.onDemandInvoicedManual && (
  205. <DetailLabel title={`Pay-as-you-go Cost-Per-Event ${categoryName}`}>
  206. {typeof categoryHistory.onDemandCpe === 'number'
  207. ? displayPriceWithCents({
  208. cents: categoryHistory.onDemandCpe,
  209. minimumFractionDigits: 8,
  210. maximumFractionDigits: 8,
  211. })
  212. : 'None'}
  213. </DetailLabel>
  214. )}
  215. {
  216. <DetailLabel title={`Gifted ${categoryName}`}>
  217. {formatReservedWithUnits(categoryHistory.free, category, {
  218. isGifted: true,
  219. })}
  220. </DetailLabel>
  221. }
  222. </DetailList>
  223. </Fragment>
  224. );
  225. })}
  226. </Fragment>
  227. );
  228. }
  229. function ReservedBudgetsData({customer}: ReservedDataProps) {
  230. if (!customer.hasReservedBudgets || !customer.reservedBudgets) {
  231. return null;
  232. }
  233. return (
  234. <Fragment>
  235. {customer.reservedBudgets.map(reservedBudget => {
  236. return (
  237. <Fragment key={reservedBudget.id}>
  238. <ReservedBudgetData customer={customer} reservedBudget={reservedBudget} />
  239. </Fragment>
  240. );
  241. })}
  242. </Fragment>
  243. );
  244. }
  245. function ReservedBudgetData({customer, reservedBudget}: ReservedBudgetProps) {
  246. const categories = Object.keys(reservedBudget.categories);
  247. if (categories.length === 0) {
  248. return null;
  249. }
  250. const shouldUseDsNames = customer.planDetails.categories.includes(
  251. DataCategory.SPANS_INDEXED
  252. );
  253. const budgetName = getReservedBudgetDisplayName({
  254. plan: customer.planDetails,
  255. categories,
  256. hadCustomDynamicSampling: shouldUseDsNames,
  257. shouldTitleCase: true,
  258. });
  259. return (
  260. <Fragment>
  261. <h6>{budgetName} Reserved Budget</h6>
  262. <DetailList>
  263. <DetailLabel title="Reserved Budget">
  264. {displayPriceWithCents({cents: reservedBudget.reservedBudget})}
  265. </DetailLabel>
  266. <DetailLabel title="Gifted Budget">
  267. {displayPriceWithCents({cents: reservedBudget.freeBudget})}
  268. </DetailLabel>
  269. <DetailLabel title="Total Used">
  270. {displayPriceWithCents({cents: reservedBudget.totalReservedSpend})} /{' '}
  271. {displayPriceWithCents({
  272. cents: reservedBudget.reservedBudget + reservedBudget.freeBudget,
  273. })}{' '}
  274. ({(reservedBudget.percentUsed * 100).toFixed(2)}%)
  275. </DetailLabel>
  276. </DetailList>
  277. </Fragment>
  278. );
  279. }
  280. type OnDemandSummaryProps = {
  281. customer: Subscription;
  282. };
  283. function OnDemandSummary({customer}: OnDemandSummaryProps) {
  284. const onDemandPeriod = `${moment(customer.onDemandPeriodStart).format('ll')} › ${moment(
  285. customer.onDemandPeriodEnd
  286. ).format('ll')}`;
  287. if (
  288. customer.supportsOnDemand &&
  289. (customer.onDemandMaxSpend || customer.onDemandSpendUsed)
  290. ) {
  291. const {onDemandBudgets} = customer;
  292. if (
  293. onDemandBudgets &&
  294. onDemandBudgets.budgetMode === OnDemandBudgetMode.PER_CATEGORY
  295. ) {
  296. return (
  297. <Fragment>
  298. {onDemandPeriod}
  299. <br />
  300. <small>
  301. <em>Per-category budget strategy</em>
  302. </small>
  303. <br />
  304. {customer.planDetails.onDemandCategories.map(category => {
  305. return (
  306. <Fragment key={`test-ondemand-${category}`}>
  307. <small>
  308. {`${getPlanCategoryName({plan: customer.planDetails, category})}: `}
  309. {`${displayPriceWithCents({
  310. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  311. cents: onDemandBudgets.usedSpends[category] ?? 0,
  312. })} / ${displayPriceWithCents({
  313. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  314. cents: onDemandBudgets.budgets[category] ?? 0,
  315. })}`}
  316. </small>
  317. <br />
  318. </Fragment>
  319. );
  320. })}
  321. <small>
  322. Total:{' '}
  323. {customer.onDemandMaxSpend > 0
  324. ? `${displayPriceWithCents({
  325. cents: customer.onDemandSpendUsed,
  326. })} / ${displayPriceWithCents({cents: customer.onDemandMaxSpend})}`
  327. : displayPriceWithCents({cents: customer.onDemandSpendUsed})}
  328. </small>
  329. </Fragment>
  330. );
  331. }
  332. return (
  333. <Fragment>
  334. {onDemandPeriod}
  335. <br />
  336. <small>
  337. <em>Shared budget strategy</em>
  338. </small>
  339. <br />
  340. <small>
  341. Total:{' '}
  342. {customer.onDemandMaxSpend > 0
  343. ? `${displayPriceWithCents({
  344. cents: customer.onDemandSpendUsed,
  345. })} / ${displayPriceWithCents({cents: customer.onDemandMaxSpend})}`
  346. : displayPriceWithCents({cents: customer.onDemandSpendUsed})}
  347. </small>
  348. </Fragment>
  349. );
  350. }
  351. return (
  352. <Fragment>
  353. {onDemandPeriod}
  354. <br />
  355. <small>
  356. <em>Disabled</em>
  357. </small>
  358. </Fragment>
  359. );
  360. }
  361. type Props = {
  362. customer: Subscription;
  363. onAction: (data: any) => void;
  364. organization: Organization;
  365. };
  366. function isWithinAcceptedMargin(
  367. effectiveSampleRate: number,
  368. desiredSampleRate: number
  369. ): boolean {
  370. const difference = Math.abs(effectiveSampleRate - desiredSampleRate);
  371. return difference >= 0 && difference <= desiredSampleRate * 0.1;
  372. }
  373. function DynamicSampling({organization}: {organization: Organization}) {
  374. if (organization.features?.includes('dynamic-sampling')) {
  375. const effectiveSampleRate = organization.effectiveSampleRate
  376. ? organization.effectiveSampleRate * 100
  377. : null;
  378. const desiredSampleRate = organization.desiredSampleRate
  379. ? organization.desiredSampleRate * 100
  380. : null;
  381. const diffSampleRate =
  382. effectiveSampleRate && desiredSampleRate
  383. ? Math.abs(effectiveSampleRate - desiredSampleRate)
  384. : null;
  385. return (
  386. <ThresholdLabel
  387. positive={
  388. effectiveSampleRate && desiredSampleRate
  389. ? isWithinAcceptedMargin(effectiveSampleRate, desiredSampleRate)
  390. : false
  391. }
  392. >
  393. {effectiveSampleRate && desiredSampleRate
  394. ? `${effectiveSampleRate.toFixed(2)}% instead of ${desiredSampleRate.toFixed(2)}% (~${diffSampleRate?.toFixed(2)}%)`
  395. : desiredSampleRate
  396. ? `${desiredSampleRate.toFixed(2)}%`
  397. : 'n/a'}
  398. </ThresholdLabel>
  399. );
  400. }
  401. return <ThresholdLabel positive={false}>Disabled</ThresholdLabel>;
  402. }
  403. function CustomerOverview({customer, onAction, organization}: Props) {
  404. let orgUrl = `/organizations/${organization.slug}/issues/`;
  405. const configFeatures = ConfigStore.get('features');
  406. const api = useApi();
  407. if (configFeatures.has('system:multi-region')) {
  408. orgUrl = `${organization.links.organizationUrl}/issues/`;
  409. }
  410. const regionMap = ConfigStore.get('regions').reduce(
  411. (acc, region) => {
  412. acc[region.url] = region.name;
  413. return acc;
  414. },
  415. {} as Record<string, string>
  416. );
  417. const region = regionMap[organization.links.regionUrl] ?? '??';
  418. const productTrialCategories = customer.canSelfServe
  419. ? PRODUCT_TRIAL_CATEGORIES.filter(category =>
  420. customer.planDetails.categories.includes(category)
  421. )
  422. : [];
  423. function updateProductTrialStatus(action: string, category: DataCategory) {
  424. const key = action + upperFirst(category);
  425. const data = {
  426. [key]: true,
  427. };
  428. api.request(`/customers/${organization.id}/`, {
  429. method: 'PUT',
  430. data,
  431. success: resp => {
  432. addSuccessMessage(`${resp.message}`);
  433. },
  434. error: (resp: ResponseMeta) => {
  435. addErrorMessage(
  436. `Error updating product trial status: ${resp.responseJSON?.message}`
  437. );
  438. },
  439. });
  440. }
  441. return (
  442. <DetailsContainer>
  443. <div>
  444. <DetailList>
  445. <DetailLabel title="Status">
  446. <CustomerStatus customer={customer} />
  447. {customer.isTrial && (
  448. <div>
  449. <small>
  450. <strong>{moment(customer.trialEnd).fromNow(true)} remaining</strong>{' '}
  451. (ends on {moment(customer.trialEnd).format('MMMM Do YYYY')})
  452. </small>
  453. </div>
  454. )}
  455. </DetailLabel>
  456. <DetailLabel title="Members">
  457. {customer.totalMembers?.toLocaleString()}
  458. </DetailLabel>
  459. <DetailLabel title="Projects">
  460. {customer.totalProjects?.toLocaleString()} / {UNLIMITED}{' '}
  461. </DetailLabel>
  462. <DetailLabel title="ARR">
  463. {formatCurrency(customer.acv ?? 0)}
  464. {customer.type === 'invoiced' && customer.billingInterval === 'annual' && (
  465. <span>
  466. {' | '}
  467. <ChangeARRAction customer={customer} onAction={onAction} />
  468. </span>
  469. )}
  470. </DetailLabel>
  471. </DetailList>
  472. <h6>Subscription</h6>
  473. <SubscriptionSummary customer={customer} onAction={onAction} />
  474. <ReservedData customer={customer} />
  475. <ReservedBudgetsData customer={customer} />
  476. <h6>PCSS</h6>
  477. <DetailList>
  478. <DetailLabel title="Custom Price PCSS">
  479. {typeof customer.customPricePcss === 'number'
  480. ? displayPriceWithCents({cents: customer.customPricePcss})
  481. : 'None'}
  482. </DetailLabel>
  483. </DetailList>
  484. <h6>Total</h6>
  485. <DetailList>
  486. <DetailLabel title="Custom Price (Total)">
  487. {typeof customer.customPrice === 'number'
  488. ? displayPriceWithCents({cents: customer.customPrice})
  489. : 'None'}
  490. </DetailLabel>
  491. </DetailList>
  492. </div>
  493. <div>
  494. <DetailList>
  495. <DetailLabel title="Short name">
  496. <ExternalLink href={orgUrl}>{customer.slug}</ExternalLink>
  497. </DetailLabel>
  498. <DetailLabel title="Internal ID">{customer.id}</DetailLabel>
  499. <DetailLabel title="Data Storage Location">{region}</DetailLabel>
  500. <DetailLabel title="Data Retention">
  501. {customer.dataRetention || '90d'}
  502. </DetailLabel>
  503. <DetailLabel title="Joined">
  504. {moment(customer.dateJoined).fromNow()}
  505. </DetailLabel>
  506. <DetailLabel title="Contact">
  507. {customer.owner ? <CustomerContact owner={customer.owner} /> : 'n/a'}{' '}
  508. </DetailLabel>
  509. <DetailLabel title="Type">{customer.type || 'n/a'}</DetailLabel>
  510. <DetailLabel title="Channel">{customer.channel || 'n/a'}</DetailLabel>
  511. <DetailLabel title="Sponsored Type">
  512. {customer.sponsoredType || 'n/a'}
  513. </DetailLabel>
  514. <DetailLabel title="Billing Country">
  515. {customer.countryCode
  516. ? (getCountryByCode(customer.countryCode)?.name ?? customer.countryCode)
  517. : 'n/a'}
  518. </DetailLabel>
  519. <DetailLabel title="Payment Source">
  520. {customer.paymentSource ? `··· ${customer.paymentSource.last4}` : 'n/a'}{' '}
  521. </DetailLabel>
  522. <DetailLabel title="Dynamic Sampling Mode">
  523. {organization.samplingMode ?? 'n/a'}
  524. </DetailLabel>
  525. <DynamicSampling organization={organization} />
  526. </DetailList>
  527. <h6>Linked Accounts</h6>
  528. <DetailList>
  529. <DetailLabel title="Stripe ID">
  530. {customer.stripeCustomerID ? (
  531. <ExternalLink
  532. href={`https://dashboard.stripe.com/customers/${customer.stripeCustomerID}`}
  533. >
  534. {customer.stripeCustomerID}
  535. </ExternalLink>
  536. ) : (
  537. 'n/a'
  538. )}
  539. </DetailLabel>
  540. <DetailLabel
  541. title={
  542. <Tooltip title="A partner account is managed by a third-party (such as Heroku).">
  543. <abbr>Partner</abbr>
  544. </Tooltip>
  545. }
  546. >
  547. {customer.partner ? (
  548. <Fragment>
  549. {customer.partner.partnership.displayName}{' '}
  550. {`(${customer.partner.isActive ? 'active' : 'migrated'})`}
  551. <br />
  552. <small>ID: {customer.partner.externalId}</small>
  553. {customer.partner.partnership.id === 'HK' && (
  554. <span>
  555. <br />
  556. <small>Heroku ID: {customer.partner.name}</small>
  557. </span>
  558. )}
  559. </Fragment>
  560. ) : (
  561. 'n/a'
  562. )}
  563. </DetailLabel>
  564. <DetailLabel title="SFDC Account">
  565. <ExternalLink
  566. href={`https://getsentry.lightning.force.com/apex/redirectToAccountPage?organizationId=${customer.id}`}
  567. >
  568. {customer.id}
  569. </ExternalLink>
  570. </DetailLabel>
  571. </DetailList>
  572. <h6>Queries</h6>
  573. <DetailList>
  574. <DetailLabel title="Looker">
  575. <ExternalLink
  576. href={`https://sentryio.cloud.looker.com/dashboards/724?Organization%20ID=${customer.id}`}
  577. >
  578. Single Org Details Dashboard
  579. </ExternalLink>
  580. </DetailLabel>
  581. <DetailLabel title="Google Cloud Logging">
  582. <ExternalLink href={getLogQuery('api', {organizationId: customer.id})}>
  583. API Logs
  584. </ExternalLink>
  585. {' | '}
  586. <ExternalLink href={getLogQuery('audit', {organizationId: customer.id})}>
  587. Audit
  588. </ExternalLink>
  589. {' | '}
  590. <ExternalLink href={getLogQuery('email', {organizationId: customer.id})}>
  591. Emails
  592. </ExternalLink>
  593. {' | '}
  594. <ExternalLink href={getLogQuery('billing', {organizationId: customer.id})}>
  595. Billing
  596. </ExternalLink>
  597. {' | '}
  598. <ExternalLink href={getLogQuery('auth', {organizationId: customer.id})}>
  599. Auth
  600. </ExternalLink>
  601. </DetailLabel>
  602. </DetailList>
  603. {productTrialCategories.length > 0 && (
  604. <Fragment>
  605. <h6>Product Trials</h6>
  606. <DetailList>
  607. {productTrialCategories.map(category => {
  608. const categoryName = titleCase(
  609. getPlanCategoryName({plan: customer.planDetails, category})
  610. );
  611. return (
  612. <DetailLabel key={category} title={categoryName}>
  613. <Button
  614. priority="link"
  615. onClick={() => updateProductTrialStatus('allowTrial', category)}
  616. >
  617. {tct('Allow [categoryName] Trial', {categoryName})}
  618. </Button>
  619. {' |'}
  620. <Button
  621. priority="link"
  622. onClick={() => updateProductTrialStatus('startTrial', category)}
  623. >
  624. {tct('Start [categoryName] Trial', {categoryName})}
  625. </Button>
  626. {' | '}
  627. <Button
  628. priority="link"
  629. onClick={() => updateProductTrialStatus('stopTrial', category)}
  630. >
  631. {tct('Stop [categoryName] Trial', {categoryName})}
  632. </Button>
  633. </DetailLabel>
  634. );
  635. })}
  636. </DetailList>
  637. </Fragment>
  638. )}
  639. </div>
  640. </DetailsContainer>
  641. );
  642. }
  643. type ThresholdLabelProps = {
  644. children: React.ReactNode;
  645. positive: boolean;
  646. };
  647. function ThresholdLabel({positive, children}: ThresholdLabelProps) {
  648. return (
  649. <Fragment>
  650. <dt>Sample Rate (24h):</dt>
  651. <ThresholdValue positive={positive}>{children}</ThresholdValue>
  652. </Fragment>
  653. );
  654. }
  655. const ThresholdValue = styled('dd')<{positive: boolean}>`
  656. color: ${p => (p.positive ? p.theme.green400 : p.theme.red400)};
  657. `;
  658. export default CustomerOverview;