Browse Source

chore(settings): Convert ProjectTags to FC and useApiQuery (#59855)

this pr converts ProjectTags to be a function component that uses
useApiQuery

https://github.com/getsentry/frontend-tsc/issues/2
Richard Roggenkemper 1 year ago
parent
commit
0c1b86f575

+ 15 - 16
static/app/views/settings/projectTags/index.spec.tsx

@@ -7,7 +7,6 @@ import {
   renderGlobalModal,
   screen,
   userEvent,
-  waitFor,
 } from 'sentry-test/reactTestingLibrary';
 
 import ProjectTags from 'sentry/views/settings/projectTags';
@@ -29,10 +28,10 @@ describe('ProjectTags', function () {
   });
 
   it('renders', function () {
-    render(<ProjectTags {...routerProps} organization={org} project={project} />);
+    render(<ProjectTags {...routerProps} />);
   });
 
-  it('renders empty', function () {
+  it('renders empty', async function () {
     MockApiClient.clearMockResponses();
     MockApiClient.addMockResponse({
       url: `/projects/${org.slug}/${project.slug}/tags/`,
@@ -40,36 +39,36 @@ describe('ProjectTags', function () {
       body: [],
     });
 
-    render(<ProjectTags {...routerProps} organization={org} project={project} />);
-    expect(screen.getByTestId('empty-message')).toBeInTheDocument();
+    render(<ProjectTags {...routerProps} />);
+    expect(await screen.findByTestId('empty-message')).toBeInTheDocument();
   });
 
-  it('disables delete button for users without access', function () {
-    render(<ProjectTags {...routerProps} organization={org} project={project} />, {
+  it('disables delete button for users without access', async function () {
+    render(<ProjectTags {...routerProps} />, {
       organization: Organization({access: []}),
     });
 
-    screen
-      .getAllByRole('button', {name: 'Remove tag'})
-      .forEach(button => expect(button).toBeDisabled());
+    (await screen.findAllByRole('button', {name: 'Remove tag'})).forEach(button =>
+      expect(button).toBeDisabled()
+    );
   });
 
   it('deletes tag', async function () {
-    render(<ProjectTags {...routerProps} organization={org} project={project} />);
+    render(<ProjectTags {...routerProps} />);
 
     // First tag exists
-    const tagCount = screen.getAllByTestId('tag-row').length;
+    const tagCount = (await screen.findAllByTestId('tag-row')).length;
+
+    expect(tagCount).toBe(5);
 
     // Remove the first tag
     await userEvent.click(screen.getAllByRole('button', {name: 'Remove tag'})[0]);
 
     // Press confirm in modal
     renderGlobalModal();
-    await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
+    await userEvent.click(await screen.findByTestId('confirm-button'));
 
     // Wait for the tag to have been removed in the store
-    await waitFor(() =>
-      expect(screen.getAllByTestId('tag-row')).toHaveLength(tagCount - 1)
-    );
+    expect(await screen.findAllByTestId('tag-row')).toHaveLength(tagCount - 1);
   });
 });

+ 126 - 116
static/app/views/settings/projectTags/index.tsx

@@ -2,145 +2,155 @@ import {Fragment} from 'react';
 import {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
 
+import {addErrorMessage} from 'sentry/actionCreators/indicator';
 import Access from 'sentry/components/acl/access';
 import {Button} from 'sentry/components/button';
 import Confirm from 'sentry/components/confirm';
 import EmptyMessage from 'sentry/components/emptyMessage';
 import ExternalLink from 'sentry/components/links/externalLink';
+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 {Organization, Project, TagWithTopValues} from 'sentry/types';
+import {TagWithTopValues} from 'sentry/types';
+import {
+  setApiQueryData,
+  useApiQuery,
+  useMutation,
+  useQueryClient,
+} from 'sentry/utils/queryClient';
+import RequestError from 'sentry/utils/requestError/requestError';
 import routeTitleGen from 'sentry/utils/routeTitle';
-import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
+import useApi from 'sentry/utils/useApi';
+import useOrganization from 'sentry/utils/useOrganization';
+import useProjects from 'sentry/utils/useProjects';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
 import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
 
-type Props = RouteComponentProps<{projectId: string}, {}> & {
-  organization: Organization;
-  project: Project;
-} & DeprecatedAsyncView['props'];
-
-type State = {
-  tags: Array<TagWithTopValues>;
-} & DeprecatedAsyncView['state'];
-
-class ProjectTags extends DeprecatedAsyncView<Props, State> {
-  getDefaultState(): State {
-    return {
-      ...super.getDefaultState(),
-      tags: [],
-    };
-  }
-
-  getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
-    const {organization} = this.props;
-    const {projectId} = this.props.params;
-    return [['tags', `/projects/${organization.slug}/${projectId}/tags/`]];
+type Props = RouteComponentProps<{projectId: string}, {}>;
+
+type DeleteTagResponse = unknown;
+type DeleteTagVariables = {key: TagWithTopValues['key']};
+
+function ProjectTags(props: Props) {
+  const organization = useOrganization();
+  const {projects} = useProjects();
+  const {projectId} = props.params;
+
+  const project = projects.find(p => p.id === projectId);
+
+  const api = useApi();
+  const queryClient = useQueryClient();
+
+  const {
+    data: tags,
+    isLoading,
+    isError,
+  } = useApiQuery<TagWithTopValues[]>(
+    [`/projects/${organization.slug}/${projectId}/tags/`],
+    {staleTime: 0}
+  );
+
+  const {mutate} = useMutation<DeleteTagResponse, RequestError, DeleteTagVariables>({
+    mutationFn: ({key}: DeleteTagVariables) =>
+      api.requestPromise(`/projects/${organization.slug}/${projectId}/tags/${key}/`, {
+        method: 'DELETE',
+      }),
+    onSuccess: (_, {key}) => {
+      setApiQueryData<TagWithTopValues[]>(
+        queryClient,
+        [`/projects/${organization.slug}/${projectId}/tags/`],
+        oldTags => oldTags.filter(tag => tag.key !== key)
+      );
+    },
+    onError: () => {
+      addErrorMessage(t('An error occurred while deleting the tag'));
+    },
+  });
+
+  if (isLoading) {
+    return <LoadingIndicator />;
   }
 
-  getTitle() {
-    const {projectId} = this.props.params;
-    return routeTitleGen(t('Tags'), projectId, false);
+  if (isError) {
+    return <LoadingError />;
   }
 
-  handleDelete = (key: TagWithTopValues['key'], idx: number) => async () => {
-    const {organization, params} = this.props;
-    const {projectId} = params;
-
-    try {
-      await this.api.requestPromise(
-        `/projects/${organization.slug}/${projectId}/tags/${key}/`,
-        {
-          method: 'DELETE',
-        }
-      );
-
-      const tags = [...this.state.tags];
-      tags.splice(idx, 1);
-      this.setState({tags});
-    } catch (error) {
-      this.setState({error: true, loading: false});
-    }
-  };
-
-  renderBody() {
-    const {project} = this.props;
-    const {tags} = this.state;
-    const isEmpty = !tags || !tags.length;
-
-    return (
-      <Fragment>
-        <SettingsPageHeader title={t('Tags')} />
-        <TextBlock>
-          {tct(
-            `Each event in Sentry may be annotated with various tags (key and value pairs).
+  const isEmpty = !tags || !tags.length;
+  return (
+    <Fragment>
+      <SentryDocumentTitle title={routeTitleGen(t('Tags'), projectId, false)} />
+      <SettingsPageHeader title={t('Tags')} />
+      <TextBlock>
+        {tct(
+          `Each event in Sentry may be annotated with various tags (key and value pairs).
                  Learn how to [link:add custom tags].`,
-            {
-              link: (
-                <ExternalLink href="https://docs.sentry.io/platform-redirect/?next=/enriching-events/tags/" />
-              ),
-            }
+          {
+            link: (
+              <ExternalLink href="https://docs.sentry.io/platform-redirect/?next=/enriching-events/tags/" />
+            ),
+          }
+        )}
+      </TextBlock>
+
+      <PermissionAlert project={project} />
+      <Panel>
+        <PanelHeader>{t('Tags')}</PanelHeader>
+        <PanelBody>
+          {isEmpty ? (
+            <EmptyMessage>
+              {tct('There are no tags, [link:learn how to add tags]', {
+                link: (
+                  <ExternalLink href="https://docs.sentry.io/product/sentry-basics/enrich-data/" />
+                ),
+              })}
+            </EmptyMessage>
+          ) : (
+            <Access access={['project:write']} project={project}>
+              {({hasAccess}) =>
+                tags.map(({key, canDelete}) => {
+                  const enabled = canDelete && hasAccess;
+                  return (
+                    <TagPanelItem key={key} data-test-id="tag-row">
+                      <TagName>{key}</TagName>
+                      <Actions>
+                        <Confirm
+                          message={t('Are you sure you want to remove this tag?')}
+                          onConfirm={() => mutate({key})}
+                          disabled={!enabled}
+                        >
+                          <Button
+                            size="xs"
+                            title={
+                              enabled
+                                ? t('Remove tag')
+                                : hasAccess
+                                ? t('This tag cannot be deleted.')
+                                : t('You do not have permission to remove tags.')
+                            }
+                            aria-label={t('Remove tag')}
+                            icon={<IconDelete size="xs" />}
+                            data-test-id="delete"
+                          />
+                        </Confirm>
+                      </Actions>
+                    </TagPanelItem>
+                  );
+                })
+              }
+            </Access>
           )}
-        </TextBlock>
-
-        <PermissionAlert project={project} />
-        <Panel>
-          <PanelHeader>{t('Tags')}</PanelHeader>
-          <PanelBody>
-            {isEmpty ? (
-              <EmptyMessage>
-                {tct('There are no tags, [link:learn how to add tags]', {
-                  link: (
-                    <ExternalLink href="https://docs.sentry.io/product/sentry-basics/enrich-data/" />
-                  ),
-                })}
-              </EmptyMessage>
-            ) : (
-              <Access access={['project:write']} project={project}>
-                {({hasAccess}) =>
-                  tags.map(({key, canDelete}, idx) => {
-                    const enabled = canDelete && hasAccess;
-                    return (
-                      <TagPanelItem key={key} data-test-id="tag-row">
-                        <TagName>{key}</TagName>
-                        <Actions>
-                          <Confirm
-                            message={t('Are you sure you want to remove this tag?')}
-                            onConfirm={this.handleDelete(key, idx)}
-                            disabled={!enabled}
-                          >
-                            <Button
-                              size="xs"
-                              title={
-                                enabled
-                                  ? t('Remove tag')
-                                  : hasAccess
-                                  ? t('This tag cannot be deleted.')
-                                  : t('You do not have permission to remove tags.')
-                              }
-                              aria-label={t('Remove tag')}
-                              icon={<IconDelete size="xs" />}
-                              data-test-id="delete"
-                            />
-                          </Confirm>
-                        </Actions>
-                      </TagPanelItem>
-                    );
-                  })
-                }
-              </Access>
-            )}
-          </PanelBody>
-        </Panel>
-      </Fragment>
-    );
-  }
+        </PanelBody>
+      </Panel>
+    </Fragment>
+  );
 }
 
 export default ProjectTags;