Browse Source

ref(tsc): Convert AccountSecurityWrapper to FC (#76720)

Convert `<AccountSecurityWrapper />` to FC.

- part of https://github.com/getsentry/frontend-tsc/issues/2
ArthurKnaus 6 months ago
parent
commit
a7039d6197

+ 14 - 14
static/app/views/settings/account/accountSecurity/accountSecurityDetails.spec.tsx

@@ -51,14 +51,14 @@ describe('AccountSecurityDetails', function () {
       const params = {
         authId: '15',
       };
-      const {routerProps, router} = initializeOrg({
+      const {router} = initializeOrg({
         router: {
           params,
         },
       });
 
       render(
-        <AccountSecurityWrapper {...routerProps}>
+        <AccountSecurityWrapper>
           <AccountSecurityDetails
             onRegenerateBackupCodes={jest.fn()}
             deleteDisabled={false}
@@ -83,14 +83,14 @@ describe('AccountSecurityDetails', function () {
       const params = {
         authId: '15',
       };
-      const {routerProps, router} = initializeOrg({
+      const {router} = initializeOrg({
         router: {
           params,
         },
       });
 
       render(
-        <AccountSecurityWrapper {...routerProps}>
+        <AccountSecurityWrapper>
           <AccountSecurityDetails
             onRegenerateBackupCodes={jest.fn()}
             deleteDisabled={false}
@@ -122,14 +122,14 @@ describe('AccountSecurityDetails', function () {
       const params = {
         authId: '15',
       };
-      const {routerProps, router} = initializeOrg({
+      const {router} = initializeOrg({
         router: {
           params,
         },
       });
 
       render(
-        <AccountSecurityWrapper {...routerProps}>
+        <AccountSecurityWrapper>
           <AccountSecurityDetails
             onRegenerateBackupCodes={jest.fn()}
             deleteDisabled={false}
@@ -162,14 +162,14 @@ describe('AccountSecurityDetails', function () {
         authId: '15',
       };
 
-      const {router, routerProps} = initializeOrg({
+      const {router} = initializeOrg({
         router: {
           params,
         },
       });
 
       render(
-        <AccountSecurityWrapper {...routerProps}>
+        <AccountSecurityWrapper>
           <AccountSecurityDetails
             onRegenerateBackupCodes={jest.fn()}
             deleteDisabled={false}
@@ -210,14 +210,14 @@ describe('AccountSecurityDetails', function () {
         authId: '16',
       };
 
-      const {routerProps, router} = initializeOrg({
+      const {router} = initializeOrg({
         router: {
           params,
         },
       });
 
       render(
-        <AccountSecurityWrapper {...routerProps}>
+        <AccountSecurityWrapper>
           <AccountSecurityDetails
             onRegenerateBackupCodes={jest.fn()}
             deleteDisabled={false}
@@ -241,14 +241,14 @@ describe('AccountSecurityDetails', function () {
         authId: '16',
       };
 
-      const {routerProps, router} = initializeOrg({
+      const {router} = initializeOrg({
         router: {
           params,
         },
       });
 
       render(
-        <AccountSecurityWrapper {...routerProps}>
+        <AccountSecurityWrapper>
           <AccountSecurityDetails
             onRegenerateBackupCodes={jest.fn()}
             deleteDisabled={false}
@@ -279,7 +279,7 @@ describe('AccountSecurityDetails', function () {
         authId: '16',
       };
 
-      const {routerProps, router} = initializeOrg({
+      const {router} = initializeOrg({
         router: {
           params,
         },
@@ -290,7 +290,7 @@ describe('AccountSecurityDetails', function () {
       });
 
       render(
-        <AccountSecurityWrapper {...routerProps}>
+        <AccountSecurityWrapper>
           <AccountSecurityDetails
             onRegenerateBackupCodes={jest.fn()}
             deleteDisabled={false}

+ 98 - 99
static/app/views/settings/account/accountSecurity/accountSecurityWrapper.tsx

@@ -1,123 +1,122 @@
-import {cloneElement} from 'react';
-import type {RouteComponentProps} from 'react-router';
+import {cloneElement, useCallback} from 'react';
 
 import {addErrorMessage} from 'sentry/actionCreators/indicator';
 import {fetchOrganizations} from 'sentry/actionCreators/organizations';
-import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
+import LoadingError from 'sentry/components/loadingError';
+import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {t} from 'sentry/locale';
 import type {Authenticator} from 'sentry/types/auth';
 import type {OrganizationSummary} from 'sentry/types/organization';
 import type {UserEmail} from 'sentry/types/user';
 import {defined} from 'sentry/utils';
+import {useApiQuery, useMutation, useQuery} from 'sentry/utils/queryClient';
+import useApi from 'sentry/utils/useApi';
+import {useParams} from 'sentry/utils/useParams';
 
 const ENDPOINT = '/users/me/authenticators/';
 
-type Props = {
+interface Props {
   children: React.ReactElement;
-} & RouteComponentProps<{authId: string}, {}> &
-  DeprecatedAsyncComponent['props'];
-
-type State = {
-  emails: UserEmail[];
-  loadingOrganizations: boolean;
-  authenticators?: Authenticator[] | null;
-  organizations?: OrganizationSummary[];
-} & DeprecatedAsyncComponent['state'];
-
-class AccountSecurityWrapper extends DeprecatedAsyncComponent<Props, State> {
-  async fetchOrganizations() {
-    try {
-      this.setState({loadingOrganizations: true});
-      const organizations = await fetchOrganizations(this.api);
-      this.setState({organizations, loadingOrganizations: false});
-    } catch (e) {
-      this.setState({error: true, loadingOrganizations: false});
-    }
-  }
-
-  get shouldRenderLoading() {
-    return super.shouldRenderLoading || this.state.loadingOrganizations === true;
-  }
-
-  getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
-    return [
-      ['authenticators', ENDPOINT],
-      ['emails', '/users/me/emails/'],
-    ];
-  }
-
-  componentDidMount() {
-    super.componentDidMount();
-    this.fetchOrganizations();
-  }
-
-  reloadData() {
-    this.fetchOrganizations();
-    super.reloadData();
-  }
-
-  handleDisable = async (auth: Authenticator) => {
-    if (!auth || !auth.authId) {
-      return;
-    }
-
-    this.setState({loading: true});
+}
 
-    try {
-      await this.api.requestPromise(`${ENDPOINT}${auth.authId}/`, {method: 'DELETE'});
-      this.reloadData();
-    } catch (_err) {
-      this.setState({loading: false});
-      addErrorMessage(t('Error disabling %s', auth.name));
+function AccountSecurityWrapper({children}: Props) {
+  const api = useApi();
+  const {authId} = useParams<{authId?: string}>();
+
+  const orgRequest = useQuery<OrganizationSummary[]>(
+    ['organizations'],
+    () => fetchOrganizations(api),
+    {staleTime: 0}
+  );
+  const emailsRequest = useApiQuery<UserEmail[]>(['/users/me/emails/'], {staleTime: 0});
+  const authenticatorsRequest = useApiQuery<Authenticator[]>([ENDPOINT], {staleTime: 0});
+
+  const handleRefresh = useCallback(() => {
+    orgRequest.refetch();
+    authenticatorsRequest.refetch();
+    emailsRequest.refetch();
+  }, [orgRequest, authenticatorsRequest, emailsRequest]);
+
+  const disableAuthenticatorMutation = useMutation(
+    async (auth: Authenticator) => {
+      if (!auth || !auth.authId) {
+        return;
+      }
+
+      await api.requestPromise(`${ENDPOINT}${auth.authId}/`, {method: 'DELETE'});
+    },
+    {
+      onSuccess: () => {
+        handleRefresh();
+      },
+      onError: (_, auth) => {
+        addErrorMessage(t('Error disabling %s', auth.name));
+      },
     }
-  };
+  );
 
-  handleRegenerateBackupCodes = async () => {
-    this.setState({loading: true});
+  const regenerateBackupCodesMutation = useMutation(
+    async () => {
+      if (!authId) {
+        return;
+      }
 
-    try {
-      await this.api.requestPromise(`${ENDPOINT}${this.props.params.authId}/`, {
+      await api.requestPromise(`${ENDPOINT}${authId}/`, {
         method: 'PUT',
       });
-      this.reloadData();
-    } catch (_err) {
-      this.setState({loading: false});
-      addErrorMessage(t('Error regenerating backup codes'));
-    }
-  };
-
-  handleRefresh = () => {
-    this.reloadData();
-  };
-
-  renderBody() {
-    const {children} = this.props;
-    const {authenticators, organizations, emails} = this.state;
-    const enrolled =
-      authenticators?.filter(auth => auth.isEnrolled && !auth.isBackupInterface) || [];
-    const countEnrolled = enrolled.length;
-    const orgsRequire2fa = organizations?.filter(org => org.require2FA) || [];
-    const deleteDisabled = orgsRequire2fa.length > 0 && countEnrolled === 1;
-    const hasVerifiedEmail = !!emails?.find(({isVerified}) => isVerified);
-
-    // This happens when you switch between children views and the next child
-    // view is lazy loaded, it can potentially be `null` while the code split
-    // package is being fetched
-    if (!defined(children)) {
-      return null;
+    },
+    {
+      onSuccess: () => {
+        handleRefresh();
+      },
+      onError: () => {
+        addErrorMessage(t('Error regenerating backup codes'));
+      },
     }
+  );
+
+  if (
+    orgRequest.isLoading ||
+    emailsRequest.isPending ||
+    authenticatorsRequest.isPending ||
+    disableAuthenticatorMutation.isLoading ||
+    regenerateBackupCodesMutation.isLoading
+  ) {
+    return <LoadingIndicator />;
+  }
+
+  if (authenticatorsRequest.isError || emailsRequest.isError || orgRequest.isError) {
+    return <LoadingError onRetry={handleRefresh} />;
+  }
 
-    return cloneElement(this.props.children, {
-      onDisable: this.handleDisable,
-      onRegenerateBackupCodes: this.handleRegenerateBackupCodes,
-      authenticators,
-      deleteDisabled,
-      orgsRequire2fa,
-      countEnrolled,
-      hasVerifiedEmail,
-      handleRefresh: this.handleRefresh,
-    });
+  const authenticators = authenticatorsRequest.data;
+  const emails = emailsRequest.data;
+  const organizations = orgRequest.data;
+
+  const enrolled =
+    authenticators.filter(auth => auth.isEnrolled && !auth.isBackupInterface) || [];
+  const countEnrolled = enrolled.length;
+  const orgsRequire2fa = organizations.filter(org => org.require2FA) || [];
+  const deleteDisabled = orgsRequire2fa.length > 0 && countEnrolled === 1;
+  const hasVerifiedEmail = !!emails.find(({isVerified}) => isVerified);
+
+  // This happens when you switch between children views and the next child
+  // view is lazy loaded, it can potentially be `null` while the code split
+  // package is being fetched
+  if (!defined(children)) {
+    return null;
   }
+
+  return cloneElement(children, {
+    onDisable: disableAuthenticatorMutation.mutate,
+    onRegenerateBackupCodes: regenerateBackupCodesMutation.mutate,
+    authenticators,
+    deleteDisabled,
+    orgsRequire2fa,
+    countEnrolled,
+    hasVerifiedEmail,
+    handleRefresh,
+  });
 }
 
 export default AccountSecurityWrapper;

+ 5 - 5
static/app/views/settings/account/accountSecurity/components/twoFactorRequired.spec.tsx

@@ -53,7 +53,7 @@ describe('TwoFactorRequired', function () {
     });
 
     render(
-      <AccountSecurityWrapper {...routerProps} params={{authId: ''}}>
+      <AccountSecurityWrapper>
         <TwoFactorRequired {...baseProps} />
       </AccountSecurityWrapper>,
       {router}
@@ -65,7 +65,7 @@ describe('TwoFactorRequired', function () {
 
   it('does not render when 2FA is disabled and no pendingInvite cookie', async function () {
     render(
-      <AccountSecurityWrapper {...routerProps} params={{authId: ''}}>
+      <AccountSecurityWrapper>
         <TwoFactorRequired {...baseProps} />
       </AccountSecurityWrapper>,
       {router}
@@ -82,7 +82,7 @@ describe('TwoFactorRequired', function () {
     });
 
     render(
-      <AccountSecurityWrapper {...routerProps} params={{authId: ''}}>
+      <AccountSecurityWrapper>
         <TwoFactorRequired {...baseProps} />
       </AccountSecurityWrapper>,
       {router}
@@ -109,7 +109,7 @@ describe('TwoFactorRequired', function () {
     });
 
     render(
-      <AccountSecurityWrapper {...routerProps} params={{authId: ''}}>
+      <AccountSecurityWrapper>
         <TwoFactorRequired {...baseProps} />
       </AccountSecurityWrapper>,
       {router}
@@ -128,7 +128,7 @@ describe('TwoFactorRequired', function () {
     });
 
     render(
-      <AccountSecurityWrapper {...routerProps} params={{authId: ''}}>
+      <AccountSecurityWrapper>
         <TwoFactorRequired {...baseProps} />
       </AccountSecurityWrapper>,
       {router}

+ 7 - 9
static/app/views/settings/account/accountSecurity/index.spec.tsx

@@ -42,14 +42,7 @@ describe('AccountSecurity', function () {
 
   function renderComponent() {
     return render(
-      <AccountSecurityWrapper
-        location={router.location}
-        route={router.routes[0]}
-        routes={router.routes}
-        router={router}
-        routeParams={router.params}
-        params={{...router.params, authId: '15'}}
-      >
+      <AccountSecurityWrapper>
         <AccountSecurity
           deleteDisabled={false}
           authenticators={[]}
@@ -65,7 +58,12 @@ describe('AccountSecurity', function () {
           routeParams={router.params}
           params={{...router.params, authId: '15'}}
         />
-      </AccountSecurityWrapper>
+      </AccountSecurityWrapper>,
+      {
+        router: {
+          params: {authId: '15'},
+        },
+      }
     );
   }