disabledMemberView.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addLoadingMessage} from 'sentry/actionCreators/indicator';
  4. import {redirectToRemainingOrganization} from 'sentry/actionCreators/organizations';
  5. import {Button} from 'sentry/components/button';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import Confirm from 'sentry/components/confirm';
  8. import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  9. import Footer from 'sentry/components/footer';
  10. import PageOverlay from 'sentry/components/pageOverlay';
  11. import {SidebarWrapper} from 'sentry/components/sidebar';
  12. import SidebarDropdown from 'sentry/components/sidebar/sidebarDropdown';
  13. import {t, tct} from 'sentry/locale';
  14. import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
  15. import type {Organization} from 'sentry/types/organization';
  16. import {sendUpgradeRequest} from 'getsentry/actionCreators/upsell';
  17. import DeactivatedMember from 'getsentry/components/features/illustrations/deactivatedMember';
  18. import withSubscription from 'getsentry/components/withSubscription';
  19. import type {Subscription} from 'getsentry/types';
  20. import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
  21. type Props = RouteComponentProps<
  22. {orgId: string},
  23. Record<PropertyKey, string | undefined>
  24. > & {
  25. subscription: Subscription;
  26. };
  27. type State = DeprecatedAsyncComponent['state'] & {
  28. organization: Organization | null;
  29. requested?: boolean;
  30. };
  31. const TextWrapper = styled('div')`
  32. max-width: 500px;
  33. margin-right: 20px;
  34. `;
  35. class DisabledMemberView extends DeprecatedAsyncComponent<Props, State> {
  36. get orgSlug() {
  37. return this.props.subscription.slug;
  38. }
  39. getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
  40. return [
  41. [
  42. 'organization',
  43. `/organizations/${this.orgSlug}/?detailed=0&include_feature_flags=1`,
  44. ],
  45. ];
  46. }
  47. componentDidMount() {
  48. super.componentDidMount();
  49. // needed to make the left margin work as expected
  50. document.body.classList.add('body-sidebar');
  51. }
  52. onLoadAllEndpointsSuccess() {
  53. const {organization, subscription} = this.state;
  54. if (organization) {
  55. trackGetsentryAnalytics('disabled_member_view.loaded', {
  56. organization,
  57. subscription,
  58. });
  59. }
  60. }
  61. handleUpgradeRequest = async () => {
  62. const {organization, subscription} = this.state;
  63. if (!organization) {
  64. return;
  65. }
  66. await sendUpgradeRequest({
  67. api: this.api,
  68. organization,
  69. type: 'disabledMember',
  70. handleSuccess: () => this.setState({requested: true}),
  71. });
  72. trackGetsentryAnalytics('disabled_member_view.clicked_upgrade_request', {
  73. organization,
  74. subscription,
  75. });
  76. };
  77. handleLeave = async () => {
  78. const {organization, subscription} = this.state;
  79. if (!organization) {
  80. return;
  81. }
  82. addLoadingMessage(t('Requesting\u2026'));
  83. try {
  84. await this.api.requestPromise(`/organizations/${organization.slug}/members/me/`, {
  85. method: 'DELETE',
  86. data: {},
  87. });
  88. } catch (err) {
  89. addErrorMessage(t('Unable to leave organization'));
  90. return;
  91. }
  92. trackGetsentryAnalytics('disabled_member_view.clicked_leave_org', {
  93. organization,
  94. subscription,
  95. });
  96. redirectToRemainingOrganization({orgId: organization.slug, removeOrg: true});
  97. };
  98. renderBody() {
  99. const {organization, requested} = this.state;
  100. const {subscription} = this.props;
  101. const totalLicenses = subscription.totalLicenses;
  102. const orgName = organization?.name;
  103. const requestButton = requested ? (
  104. <strong>{t('Requested!')}</strong>
  105. ) : (
  106. <Button onClick={this.handleUpgradeRequest} size="sm" priority="primary">
  107. {t('Request Upgrade')}
  108. </Button>
  109. );
  110. return (
  111. <PageContainer>
  112. <MinimalistSidebar collapsed={false}>
  113. {organization && (
  114. <SidebarDropdown orientation="left" collapsed={false} hideOrgLinks />
  115. )}
  116. </MinimalistSidebar>
  117. {organization && (
  118. <PageOverlay
  119. background={DeactivatedMember}
  120. customWrapper={TextWrapper}
  121. text={({Header, Body}) => (
  122. <Fragment>
  123. <Header>{t('Member Deactivated')}</Header>
  124. <Body>
  125. <p>
  126. {tct(
  127. '[firstSentence] Request an upgrade to our Team or Business Plan to get back to making software less bad.',
  128. {
  129. firstSentence:
  130. totalLicenses > 1
  131. ? tct(
  132. '[orgName] is on a plan that supports only [totalLicenses] members.',
  133. {
  134. orgName: <strong>{organization.name}</strong>,
  135. totalLicenses,
  136. }
  137. )
  138. : tct('[orgName] is on a plan that supports only 1 member.', {
  139. orgName: <strong>{organization.name}</strong>,
  140. }),
  141. }
  142. )}
  143. </p>
  144. <DisabledMemberButtonBar gap={2}>
  145. {requestButton}
  146. <Confirm
  147. onConfirm={this.handleLeave}
  148. message={tct('Are you sure you want to leave [orgName]?', {
  149. orgName,
  150. })}
  151. >
  152. <Button size="sm" priority="danger">
  153. {t('Leave')}
  154. </Button>
  155. </Confirm>
  156. </DisabledMemberButtonBar>
  157. </Body>
  158. </Fragment>
  159. )}
  160. positioningStrategy={({mainRect, anchorRect, wrapperRect}) => {
  161. // Vertically center within the anchor
  162. let y =
  163. (anchorRect.height - wrapperRect.height + 40) / 2 +
  164. anchorRect.y -
  165. mainRect.y;
  166. // move up text on mobile
  167. if (mainRect.width < 480) {
  168. y = y - 100;
  169. }
  170. // Align to the right of the anchor, avoid overflowing outside of the
  171. // page, the best we can do is start to overlap the illustration at
  172. // this point.
  173. let x = anchorRect.x - mainRect.x - wrapperRect.width;
  174. x = Math.max(30, x);
  175. return {x, y};
  176. }}
  177. />
  178. )}
  179. <Footer />
  180. </PageContainer>
  181. );
  182. }
  183. }
  184. export default withSubscription(DisabledMemberView);
  185. const MinimalistSidebar = styled(SidebarWrapper)`
  186. padding: 12px 19px;
  187. `;
  188. const PageContainer = styled('div')`
  189. display: flex;
  190. flex-direction: column;
  191. flex-grow: 1;
  192. min-height: 100vh;
  193. `;
  194. const DisabledMemberButtonBar = styled(ButtonBar)`
  195. max-width: fit-content;
  196. `;