Browse Source

ref(tsc): convert accountAuthorizations to FC (#74381)

ref https://github.com/getsentry/frontend-tsc/issues/2

converts this file into FC and use `useApiQuery` instead of
`DeprecatedAsync`

recommend viewing with hide whitespace
Michelle Zhang 7 months ago
parent
commit
badf622261

+ 51 - 14
static/app/views/settings/account/accountAuthorizations.spec.tsx

@@ -1,7 +1,6 @@
-import {LocationFixture} from 'sentry-fixture/locationFixture';
-import {RouterFixture} from 'sentry-fixture/routerFixture';
+import {ApiApplicationFixture} from 'sentry-fixture/apiApplication';
 
-import {render} from 'sentry-test/reactTestingLibrary';
+import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
 
 import AccountAuthorizations from 'sentry/views/settings/account/accountAuthorizations';
 
@@ -10,23 +9,61 @@ describe('AccountAuthorizations', function () {
     MockApiClient.clearMockResponses();
   });
 
-  it('renders empty', function () {
+  it('renders empty', async function () {
     MockApiClient.addMockResponse({
       url: '/api-authorizations/',
       method: 'GET',
       body: [],
     });
 
-    const router = RouterFixture();
-    render(
-      <AccountAuthorizations
-        location={LocationFixture()}
-        routeParams={router.params}
-        params={router.params}
-        routes={router.routes}
-        route={router.routes[0]}
-        router={router}
-      />
+    render(<AccountAuthorizations />);
+    expect(
+      await screen.findByText("You haven't approved any third party applications.")
+    ).toBeInTheDocument();
+  });
+
+  it('revokes authorizations correctly', async function () {
+    MockApiClient.addMockResponse({
+      url: '/api-authorizations/',
+      method: 'GET',
+      body: [
+        {
+          application: ApiApplicationFixture({name: 'Delete Shrimp'}),
+          homepageUrl: 'test.com',
+          id: 'delete_shrimp',
+          scopes: [],
+        },
+        {
+          application: ApiApplicationFixture({name: 'Keep Shrimp'}),
+          homepageUrl: 'test2.com',
+          id: 'keep_shrimp',
+          scopes: [],
+        },
+      ],
+    });
+    const deleteMock = MockApiClient.addMockResponse({
+      url: '/api-authorizations/',
+      method: 'DELETE',
+    });
+
+    render(<AccountAuthorizations />);
+    expect(await screen.findByText('Delete Shrimp')).toBeInTheDocument();
+    expect(await screen.findByText('Keep Shrimp')).toBeInTheDocument();
+
+    // delete the 'Detete Shrimp' authorization
+    await userEvent.click(screen.getByTestId('delete_shrimp'));
+
+    expect(deleteMock).toHaveBeenCalledWith(
+      '/api-authorizations/',
+      expect.objectContaining({
+        method: 'DELETE',
+        data: {authorization: 'delete_shrimp'},
+      })
+    );
+
+    await waitFor(() =>
+      expect(screen.queryByText('Delete Shrimp')).not.toBeInTheDocument()
     );
+    await waitFor(() => expect(screen.queryByText('Keep Shrimp')).toBeInTheDocument());
   });
 });

+ 80 - 85
static/app/views/settings/account/accountAuthorizations.tsx

@@ -1,4 +1,3 @@
-import type {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
 
 import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
@@ -6,15 +5,19 @@ import {Button} from 'sentry/components/button';
 import EmptyMessage from 'sentry/components/emptyMessage';
 import ExternalLink from 'sentry/components/links/externalLink';
 import Link from 'sentry/components/links/link';
+import LoadingError from 'sentry/components/loadingError';
+import LoadingIndicator from 'sentry/components/loadingIndicator';
 import Panel from 'sentry/components/panels/panel';
 import PanelBody from 'sentry/components/panels/panelBody';
 import PanelHeader from 'sentry/components/panels/panelHeader';
 import PanelItem from 'sentry/components/panels/panelItem';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {IconDelete} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {ApiApplication} from 'sentry/types';
-import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
+import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
+import useApi from 'sentry/utils/useApi';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 
 type Authorization = {
@@ -24,98 +27,90 @@ type Authorization = {
   scopes: string[];
 };
 
-type Props = RouteComponentProps<{}, {}>;
+function AccountAuthorizations() {
+  const api = useApi();
+  const queryClient = useQueryClient();
+  const ENDPOINT = '/api-authorizations/';
 
-type State = {
-  data: Authorization[];
-} & DeprecatedAsyncView['state'];
+  const {data, isLoading, isError, refetch} = useApiQuery<Authorization[]>([ENDPOINT], {
+    staleTime: 0,
+  });
 
-class AccountAuthorizations extends DeprecatedAsyncView<Props, State> {
-  getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
-    return [['data', '/api-authorizations/']];
+  if (isLoading) {
+    return <LoadingIndicator />;
   }
 
-  getTitle() {
-    return 'Approved Applications';
+  if (isError) {
+    return <LoadingError onRetry={refetch} />;
   }
 
-  handleRevoke = authorization => {
-    const oldData = this.state.data;
-
-    this.setState(
-      state => ({
-        data: state.data.filter(({id}) => id !== authorization.id),
-      }),
-      async () => {
-        try {
-          await this.api.requestPromise('/api-authorizations/', {
-            method: 'DELETE',
-            data: {authorization: authorization.id},
-          });
-
-          addSuccessMessage(t('Saved changes'));
-        } catch (_err) {
-          this.setState({
-            data: oldData,
-          });
-          addErrorMessage(t('Unable to save changes, please try again'));
-        }
-      }
+  const handleRevoke = async (authorization: Authorization) => {
+    const oldData = data;
+    setApiQueryData<Authorization[]>(queryClient, [ENDPOINT], prevData =>
+      prevData.filter(a => a.id !== authorization.id)
     );
+    try {
+      await api.requestPromise('/api-authorizations/', {
+        method: 'DELETE',
+        data: {authorization: authorization.id},
+      });
+      addSuccessMessage(t('Saved changes'));
+    } catch (_err) {
+      setApiQueryData<any>(queryClient, [ENDPOINT], oldData);
+      addErrorMessage(t('Unable to save changes, please try again'));
+    }
   };
 
-  renderBody() {
-    const {data} = this.state;
-    const isEmpty = data.length === 0;
-    return (
-      <div>
-        <SettingsPageHeader title="Authorized Applications" />
-        <Description>
-          {tct('You can manage your own applications via the [link:API dashboard].', {
-            link: <Link to="/settings/account/api/" />,
-          })}
-        </Description>
-
-        <Panel>
-          <PanelHeader>{t('Approved Applications')}</PanelHeader>
-
-          <PanelBody>
-            {isEmpty && (
-              <EmptyMessage>
-                {t("You haven't approved any third party applications.")}
-              </EmptyMessage>
-            )}
-
-            {!isEmpty && (
-              <div>
-                {data.map(authorization => (
-                  <PanelItemCenter key={authorization.id}>
-                    <ApplicationDetails>
-                      <ApplicationName>{authorization.application.name}</ApplicationName>
-                      {authorization.homepageUrl && (
-                        <Url>
-                          <ExternalLink href={authorization.homepageUrl}>
-                            {authorization.homepageUrl}
-                          </ExternalLink>
-                        </Url>
-                      )}
-                      <Scopes>{authorization.scopes.join(', ')}</Scopes>
-                    </ApplicationDetails>
-                    <Button
-                      size="sm"
-                      onClick={() => this.handleRevoke(authorization)}
-                      icon={<IconDelete />}
-                      aria-label={t('Delete')}
-                    />
-                  </PanelItemCenter>
-                ))}
-              </div>
-            )}
-          </PanelBody>
-        </Panel>
-      </div>
-    );
-  }
+  const isEmpty = data.length === 0;
+  return (
+    <SentryDocumentTitle title={t('Approved Applications')}>
+      <SettingsPageHeader title="Authorized Applications" />
+      <Description>
+        {tct('You can manage your own applications via the [link:API dashboard].', {
+          link: <Link to="/settings/account/api/" />,
+        })}
+      </Description>
+
+      <Panel>
+        <PanelHeader>{t('Approved Applications')}</PanelHeader>
+
+        <PanelBody>
+          {isEmpty && (
+            <EmptyMessage>
+              {t("You haven't approved any third party applications.")}
+            </EmptyMessage>
+          )}
+
+          {!isEmpty && (
+            <div>
+              {data.map(authorization => (
+                <PanelItemCenter key={authorization.id}>
+                  <ApplicationDetails>
+                    <ApplicationName>{authorization.application.name}</ApplicationName>
+                    {authorization.homepageUrl && (
+                      <Url>
+                        <ExternalLink href={authorization.homepageUrl}>
+                          {authorization.homepageUrl}
+                        </ExternalLink>
+                      </Url>
+                    )}
+                    <Scopes>{authorization.scopes.join(', ')}</Scopes>
+                  </ApplicationDetails>
+                  <Button
+                    size="sm"
+                    onClick={() => handleRevoke(authorization)}
+                    icon={<IconDelete />}
+                    aria-label={t('Delete')}
+                    data-test-id={authorization.id}
+                  />
+                </PanelItemCenter>
+              ))}
+            </div>
+          )}
+        </PanelBody>
+      </Panel>
+    </SentryDocumentTitle>
+  );
 }
 
 export default AccountAuthorizations;