Browse Source

ref(tsc): Convert ApiApplicationDetails to FC (#76658)

Convert `<ApiApplicationDetails />` to FC.

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

+ 18 - 29
static/app/views/settings/account/apiApplications/details.spec.tsx

@@ -1,4 +1,3 @@
-import {initializeOrg} from 'sentry-test/initializeOrg';
 import {
   render,
   renderGlobalModal,
@@ -9,9 +8,7 @@ import {
 import ApiApplicationDetails from 'sentry/views/settings/account/apiApplications/details';
 
 describe('ApiApplications', function () {
-  it('renders basic details for newly created App', function () {
-    const {router} = initializeOrg();
-
+  it('renders basic details for newly created App', async function () {
     MockApiClient.addMockResponse({
       url: '/api-applications/abcd/',
       body: {
@@ -27,18 +24,17 @@ describe('ApiApplications', function () {
       },
     });
 
-    render(
-      <ApiApplicationDetails
-        router={router}
-        location={router.location}
-        routes={router.routes}
-        route={{}}
-        routeParams={{}}
-        params={{
+    render(<ApiApplicationDetails />, {
+      router: {
+        params: {
           appId: 'abcd',
-        }}
-      />
-    );
+        },
+      },
+    });
+
+    expect(
+      await screen.findByRole('heading', {name: 'Application Details'})
+    ).toBeInTheDocument();
     expect(screen.getByDisplayValue('http://example.com')).toBeInTheDocument();
     expect(screen.getByDisplayValue('http://example.com/redirect')).toBeInTheDocument();
     expect(screen.getByDisplayValue('http://example.com/privacy')).toBeInTheDocument();
@@ -60,8 +56,6 @@ describe('ApiApplications', function () {
   });
 
   it('handles client secret rotation', async function () {
-    const {router} = initializeOrg();
-
     MockApiClient.addMockResponse({
       url: '/api-applications/abcd/',
       body: {
@@ -84,21 +78,16 @@ describe('ApiApplications', function () {
       },
     });
 
-    render(
-      <ApiApplicationDetails
-        router={router}
-        location={router.location}
-        routes={router.routes}
-        route={{}}
-        routeParams={{}}
-        params={{
+    render(<ApiApplicationDetails />, {
+      router: {
+        params: {
           appId: 'abcd',
-        }}
-      />
-    );
+        },
+      },
+    });
     renderGlobalModal();
 
-    expect(screen.getByText('hidden')).toBeInTheDocument();
+    expect(await screen.findByText('hidden')).toBeInTheDocument();
     expect(
       screen.getByRole('button', {name: 'Rotate client secret'})
     ).toBeInTheDocument();

+ 136 - 108
static/app/views/settings/account/apiApplications/details.tsx

@@ -1,5 +1,4 @@
 import {Fragment} from 'react';
-import type {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
 
 import {addErrorMessage} from 'sentry/actionCreators/indicator';
@@ -10,131 +9,160 @@ import Confirm from 'sentry/components/confirm';
 import Form from 'sentry/components/forms/form';
 import FormField from 'sentry/components/forms/formField';
 import JsonForm from 'sentry/components/forms/jsonForm';
+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 SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import TextCopyInput from 'sentry/components/textCopyInput';
 import apiApplication from 'sentry/data/forms/apiApplication';
 import {t} from 'sentry/locale';
 import ConfigStore from 'sentry/stores/configStore';
 import type {ApiApplication} from 'sentry/types/user';
 import getDynamicText from 'sentry/utils/getDynamicText';
-import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
+import {
+  type ApiQueryKey,
+  useApiQuery,
+  useMutation,
+  useQueryClient,
+} from 'sentry/utils/queryClient';
+import useApi from 'sentry/utils/useApi';
+import {useParams} from 'sentry/utils/useParams';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 
-type Props = RouteComponentProps<{appId: string}, {}>;
-type State = {
-  app: ApiApplication;
-} & DeprecatedAsyncView['state'];
-
-class ApiApplicationsDetails extends DeprecatedAsyncView<Props, State> {
-  rotateClientSecret = async () => {
-    try {
-      const rotateResponse = await this.api.requestPromise(
-        `/api-applications/${this.props.params.appId}/rotate-secret/`,
-        {
-          method: 'POST',
-        }
-      );
-      openModal(({Body, Header}) => (
-        <Fragment>
-          <Header>{t('Your new Client Secret')}</Header>
-          <Body>
-            <Alert type="info" showIcon>
-              {t('This will be the only time your client secret is visible!')}
-            </Alert>
-            <TextCopyInput aria-label={t('new-client-secret')}>
-              {rotateResponse.clientSecret}
-            </TextCopyInput>
-          </Body>
-        </Fragment>
-      ));
-    } catch {
-      addErrorMessage(t('Error rotating secret'));
+const PAGE_TITLE = t('Application Details');
+
+function getAppQueryKey(appId: string): ApiQueryKey {
+  return [`/api-applications/${appId}/`];
+}
+
+interface RotateClientSecretResponse {
+  clientSecret: string;
+}
+
+function ApiApplicationsDetails() {
+  const api = useApi();
+  const {appId} = useParams<{appId: string}>();
+  const queryClient = useQueryClient();
+
+  const urlPrefix = ConfigStore.get('urlPrefix');
+
+  const {
+    data: app,
+    isPending,
+    isError,
+    refetch,
+  } = useApiQuery<ApiApplication>(getAppQueryKey(appId), {
+    staleTime: 0,
+  });
+
+  const {mutate: rotateClientSecret} = useMutation<RotateClientSecretResponse>(
+    () => {
+      return api.requestPromise(`/api-applications/${appId}/rotate-secret/`, {
+        method: 'POST',
+      });
+    },
+    {
+      onSuccess: data => {
+        openModal(({Body, Header}) => (
+          <Fragment>
+            <Header>{t('Your new Client Secret')}</Header>
+            <Body>
+              <Alert type="info" showIcon>
+                {t('This will be the only time your client secret is visible!')}
+              </Alert>
+              <TextCopyInput aria-label={t('new-client-secret')}>
+                {data.clientSecret}
+              </TextCopyInput>
+            </Body>
+          </Fragment>
+        ));
+      },
+      onError: () => {
+        addErrorMessage(t('Error rotating secret'));
+      },
+      onSettled: () => {
+        queryClient.invalidateQueries(getAppQueryKey(appId));
+      },
     }
-  };
+  );
 
-  getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
-    return [['app', `/api-applications/${this.props.params.appId}/`]];
+  if (isPending) {
+    return <LoadingIndicator />;
   }
 
-  getTitle() {
-    return t('Application Details');
+  if (isError) {
+    return <LoadingError onRetry={refetch} />;
   }
 
-  renderBody() {
-    const urlPrefix = ConfigStore.get('urlPrefix');
-
-    return (
-      <div>
-        <SettingsPageHeader title={this.getTitle()} />
-
-        <Form
-          apiMethod="PUT"
-          apiEndpoint={`/api-applications/${this.props.params.appId}/`}
-          saveOnBlur
-          allowUndo
-          initialData={this.state.app}
-          onSubmitError={() => addErrorMessage('Unable to save change')}
-        >
-          <JsonForm forms={apiApplication} />
-
-          <Panel>
-            <PanelHeader>{t('Credentials')}</PanelHeader>
-
-            <PanelBody>
-              <FormField name="clientID" label="Client ID">
-                {({value}) => (
-                  <div>
-                    <TextCopyInput>
-                      {getDynamicText({value, fixed: 'CI_CLIENT_ID'})}
-                    </TextCopyInput>
-                  </div>
-                )}
-              </FormField>
-
-              <FormField
-                name="clientSecret"
-                label="Client Secret"
-                help={t(`Your secret is only available briefly after application creation. Make
+  return (
+    <SentryDocumentTitle title={PAGE_TITLE}>
+      <SettingsPageHeader title={PAGE_TITLE} />
+
+      <Form
+        apiMethod="PUT"
+        apiEndpoint={`/api-applications/${appId}/`}
+        saveOnBlur
+        allowUndo
+        initialData={app}
+        onSubmitError={() => addErrorMessage('Unable to save change')}
+      >
+        <JsonForm forms={apiApplication} />
+
+        <Panel>
+          <PanelHeader>{t('Credentials')}</PanelHeader>
+
+          <PanelBody>
+            <FormField name="clientID" label="Client ID">
+              {({value}) => (
+                <TextCopyInput>
+                  {getDynamicText({value, fixed: 'CI_CLIENT_ID'})}
+                </TextCopyInput>
+              )}
+            </FormField>
+
+            <FormField
+              name="clientSecret"
+              label="Client Secret"
+              help={t(`Your secret is only available briefly after application creation. Make
                   sure to save this value!`)}
-              >
-                {({value}) =>
-                  value ? (
-                    <TextCopyInput>
-                      {getDynamicText({value, fixed: 'CI_CLIENT_SECRET'})}
-                    </TextCopyInput>
-                  ) : (
-                    <ClientSecret>
-                      <HiddenSecret>{t('hidden')}</HiddenSecret>
-                      <Confirm
-                        onConfirm={this.rotateClientSecret}
-                        message={t(
-                          'Are you sure you want to rotate the client secret? The current one will not be usable anymore, and this cannot be undone.'
-                        )}
-                      >
-                        <Button size="xs" priority="danger">
-                          Rotate client secret
-                        </Button>
-                      </Confirm>
-                    </ClientSecret>
-                  )
-                }
-              </FormField>
-
-              <FormField name="" label="Authorization URL">
-                {() => <TextCopyInput>{`${urlPrefix}/oauth/authorize/`}</TextCopyInput>}
-              </FormField>
-
-              <FormField name="" label="Token URL">
-                {() => <TextCopyInput>{`${urlPrefix}/oauth/token/`}</TextCopyInput>}
-              </FormField>
-            </PanelBody>
-          </Panel>
-        </Form>
-      </div>
-    );
-  }
+            >
+              {({value}) =>
+                value ? (
+                  <TextCopyInput>
+                    {getDynamicText({value, fixed: 'CI_CLIENT_SECRET'})}
+                  </TextCopyInput>
+                ) : (
+                  <ClientSecret>
+                    <HiddenSecret>{t('hidden')}</HiddenSecret>
+                    <Confirm
+                      onConfirm={rotateClientSecret}
+                      message={t(
+                        'Are you sure you want to rotate the client secret? The current one will not be usable anymore, and this cannot be undone.'
+                      )}
+                    >
+                      <Button size="xs" priority="danger">
+                        Rotate client secret
+                      </Button>
+                    </Confirm>
+                  </ClientSecret>
+                )
+              }
+            </FormField>
+
+            <FormField name="" label="Authorization URL">
+              {() => <TextCopyInput>{`${urlPrefix}/oauth/authorize/`}</TextCopyInput>}
+            </FormField>
+
+            <FormField name="" label="Token URL">
+              {() => <TextCopyInput>{`${urlPrefix}/oauth/token/`}</TextCopyInput>}
+            </FormField>
+          </PanelBody>
+        </Panel>
+      </Form>
+    </SentryDocumentTitle>
+  );
 }
 
 const HiddenSecret = styled('span')`