Browse Source

ref(auditlog): Update auditlog to functional component (#36393)

* ref(auditlog): Update auditlog to functional component

* Fix failing test.

* Remove cursor and event args

* Update auditLogView test to RTL

* Additional auditLogView text test

Co-authored-by: Mark Story <mark@mark-story.com>
Maggie Bauer 2 years ago
parent
commit
ceb8ed4b63

+ 26 - 19
static/app/views/settings/organizationAuditLog/auditLogList.tsx

@@ -3,8 +3,8 @@ import styled from '@emotion/styled';
 
 import UserAvatar from 'sentry/components/avatar/userAvatar';
 import DateTime from 'sentry/components/dateTime';
-import SelectField from 'sentry/components/deprecatedforms/selectField';
-import Pagination from 'sentry/components/pagination';
+import SelectControl from 'sentry/components/forms/selectControl';
+import Pagination, {CursorHandler} from 'sentry/components/pagination';
 import {PanelTable} from 'sentry/components/panels';
 import Tooltip from 'sentry/components/tooltip';
 import {t} from 'sentry/locale';
@@ -21,9 +21,10 @@ const avatarStyle = {
 
 type Props = {
   entries: AuditLog[] | null;
-  eventType: string;
+  eventType: string | undefined;
   eventTypes: string[];
   isLoading: boolean;
+  onCursor: CursorHandler | undefined;
   onEventSelect: (value: string) => void;
   pageLinks: string | null;
 };
@@ -32,28 +33,30 @@ const AuditLogList = ({
   isLoading,
   pageLinks,
   entries,
-  eventType,
   eventTypes,
+  onCursor,
   onEventSelect,
 }: Props) => {
   const is24Hours = shouldUse24Hours();
   const hasEntries = entries && entries.length > 0;
   const ipv4Length = 15;
-  const options = [
-    {value: '', label: t('Any action'), clearableValue: false},
-    ...eventTypes.map(type => ({label: type, value: type, clearableValue: false})),
-  ];
+
+  const eventOptions = eventTypes.map(type => ({
+    label: type,
+    value: type,
+  }));
 
   const action = (
-    <form>
-      <SelectField
-        name="event"
-        onChange={onEventSelect as SelectField['props']['onChange']}
-        value={eventType}
-        style={{width: 250}}
-        options={options}
-      />
-    </form>
+    <EventSelector
+      clearable
+      isDisabled={isLoading}
+      name="eventFilter"
+      placeholder={t('Select Action: ')}
+      options={eventOptions}
+      onChange={options => {
+        onEventSelect(options?.value);
+      }}
+    />
   );
 
   return (
@@ -61,7 +64,7 @@ const AuditLogList = ({
       <SettingsPageHeader title={t('Audit Log')} action={action} />
       <PanelTable
         headers={[t('Member'), t('Action'), t('IP'), t('Time')]}
-        isEmpty={!hasEntries}
+        isEmpty={!hasEntries && entries?.length === 0}
         emptyMessage={t('No audit entries available')}
         isLoading={isLoading}
       >
@@ -108,11 +111,15 @@ const AuditLogList = ({
           </Fragment>
         ))}
       </PanelTable>
-      {pageLinks && <Pagination pageLinks={pageLinks} />}
+      {pageLinks && <Pagination pageLinks={pageLinks} onCursor={onCursor} />}
     </div>
   );
 };
 
+const EventSelector = styled(SelectControl)`
+  width: 250px;
+`;
+
 const UserInfo = styled('div')`
   display: flex;
   align-items: center;

+ 78 - 48
static/app/views/settings/organizationAuditLog/index.tsx

@@ -1,10 +1,11 @@
-import {browserHistory, RouteComponentProps} from 'react-router';
+import {Fragment, useCallback, useEffect, useState} from 'react';
+import * as Sentry from '@sentry/react';
 
-import {t} from 'sentry/locale';
+import {addErrorMessage} from 'sentry/actionCreators/indicator';
+import {CursorHandler} from 'sentry/components/pagination';
 import {AuditLog, Organization} from 'sentry/types';
-import routeTitleGen from 'sentry/utils/routeTitle';
+import useApi from 'sentry/utils/useApi';
 import withOrganization from 'sentry/utils/withOrganization';
-import AsyncView from 'sentry/views/asyncView';
 
 import AuditLogList from './auditLogList';
 
@@ -69,64 +70,93 @@ const EVENT_TYPES = [
   'sentry-app.uninstall',
 ];
 
-type Props = RouteComponentProps<{orgId: string}, {}> &
-  AsyncView['props'] & {
-    organization: Organization;
-  };
+type Props = {
+  organization: Organization;
+};
 
-type State = AsyncView['state'] & {
+type State = {
   entryList: AuditLog[] | null;
   entryListPageLinks: string | null;
+  isLoading: boolean;
+  currentCursor?: string;
+  eventType?: string;
 };
 
-class OrganizationAuditLog extends AsyncView<Props, State> {
-  getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
-    return [
-      [
-        'entryList',
-        `/organizations/${this.props.params.orgId}/audit-logs/`,
-        {
-          query: this.props.location.query,
-        },
-      ],
-    ];
-  }
+function OrganizationAuditLog({organization}: Props) {
+  const [state, setState] = useState<State>({
+    entryList: [],
+    entryListPageLinks: null,
+    isLoading: true,
+  });
+
+  const api = useApi();
 
-  getTitle() {
-    return routeTitleGen(t('Audit Log'), this.props.organization.slug, false);
-  }
+  const handleCursor: CursorHandler = resultsCursor => {
+    setState(prevState => ({
+      ...prevState,
+      currentCursor: resultsCursor,
+    }));
+  };
 
-  handleEventSelect = (value: string) => {
-    // Dont update if event has not changed
-    if (this.props.location.query.event === value) {
-      return;
+  const fetchAuditLogData = useCallback(async () => {
+    try {
+      const payload = {cursor: state.currentCursor, event: state.eventType};
+      if (!payload.cursor) {
+        delete payload.cursor;
+      }
+      if (!payload.event) {
+        delete payload.event;
+      }
+      const [data, _, response] = await api.requestPromise(
+        `/organizations/${organization.slug}/audit-logs/`,
+        {
+          method: 'GET',
+          includeAllArgs: true,
+          query: payload,
+        }
+      );
+      setState(prevState => ({
+        ...prevState,
+        entryList: data,
+        isLoading: false,
+        entryListPageLinks: response?.getResponseHeader('Link') ?? null,
+      }));
+    } catch (err) {
+      if (err.status !== 401 && err.status !== 403) {
+        Sentry.captureException(err);
+      }
+      setState(prevState => ({
+        ...prevState,
+        isLoading: false,
+      }));
+      addErrorMessage('Unable to load audit logs.');
     }
+  }, [api, organization.slug, state.currentCursor, state.eventType]);
 
-    browserHistory.push({
-      pathname: this.props.location.pathname,
-      search: `?event=${value}`,
-    });
-  };
+  useEffect(() => {
+    fetchAuditLogData();
+  }, [fetchAuditLogData]);
 
-  renderLoading() {
-    return this.renderBody();
-  }
+  const handleEventSelect = (value: string | undefined) => {
+    setState(prevState => ({
+      ...prevState,
+      eventType: value,
+    }));
+  };
 
-  renderBody() {
-    const {entryList, entryListPageLinks, loading, reloading} = this.state;
-    const currentEventType = this.props.location.query.event;
-    return (
+  return (
+    <Fragment>
       <AuditLogList
-        entries={entryList}
-        pageLinks={entryListPageLinks}
-        eventType={currentEventType}
+        entries={state.entryList}
+        pageLinks={state.entryListPageLinks}
+        eventType={state.eventType}
         eventTypes={EVENT_TYPES}
-        onEventSelect={this.handleEventSelect}
-        isLoading={loading || reloading}
-        {...this.props}
+        onEventSelect={handleEventSelect}
+        isLoading={state.isLoading}
+        onCursor={handleCursor}
       />
-    );
-  }
+    </Fragment>
+  );
 }
 
 export default withOrganization(OrganizationAuditLog);

+ 43 - 22
tests/js/spec/views/settings/auditLogView.spec.jsx

@@ -1,10 +1,17 @@
-import {mountWithTheme} from 'sentry-test/enzyme';
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {render, screen} from 'sentry-test/reactTestingLibrary';
 
 import {Client} from 'sentry/api';
 import OrganizationAuditLog from 'sentry/views/settings/organizationAuditLog';
 
-describe('OrganizationAuditLog', function () {
-  const org = TestStubs.Organization();
+describe('OrganizationAuditLog', () => {
+  const {routerContext, org} = initializeOrg({
+    projects: [],
+    router: {
+      location: {query: {}},
+      params: {orgId: 'org-slug'},
+    },
+  });
   const ENDPOINT = `/organizations/${org.slug}/audit-logs/`;
 
   beforeEach(function () {
@@ -15,27 +22,41 @@ describe('OrganizationAuditLog', function () {
     });
   });
 
-  it('renders', async function () {
-    const wrapper = mountWithTheme(
-      <OrganizationAuditLog location={{query: ''}} params={{orgId: org.slug}} />
-    );
-    wrapper.setState({loading: false});
-    wrapper.update();
-    await tick();
+  it('renders', async () => {
+    render(<OrganizationAuditLog location={{query: ''}} params={{orgId: org.slug}} />, {
+      context: routerContext,
+    });
 
-    wrapper.update();
-    expect(wrapper).toSnapshot();
+    expect(await screen.findByRole('heading')).toHaveTextContent('Audit Log');
+    expect(screen.getByRole('textbox')).toBeInTheDocument();
+    expect(screen.getByText('Member')).toBeInTheDocument();
+    expect(screen.getByText('Action')).toBeInTheDocument();
+    expect(screen.getByText('IP')).toBeInTheDocument();
+    expect(screen.getByText('Time')).toBeInTheDocument();
+    expect(screen.queryByText('No audit entries available')).not.toBeInTheDocument();
+    expect(screen.getByText('edited project ludic-science')).toBeInTheDocument();
   });
 
-  it('displays whether an action was done by a superuser', function () {
-    const wrapper = mountWithTheme(
-      <OrganizationAuditLog location={{query: ''}} params={{orgId: org.slug}} />
-    );
-    expect(wrapper.find('div[data-test-id="actor-name"]').at(0).text()).toEqual(
-      expect.stringContaining('(Sentry Staff)')
-    );
-    expect(wrapper.find('div[data-test-id="actor-name"]').at(1).text()).toEqual(
-      expect.not.stringContaining('(Sentry Staff)')
-    );
+  it('renders empty', async () => {
+    Client.clearMockResponses();
+    Client.addMockResponse({
+      url: ENDPOINT,
+      body: [],
+    });
+
+    render(<OrganizationAuditLog location={{query: ''}} params={{orgId: org.slug}} />, {
+      context: routerContext,
+    });
+
+    expect(await screen.findByText('No audit entries available')).toBeInTheDocument();
+  });
+
+  it('displays whether an action was done by a superuser', async () => {
+    render(<OrganizationAuditLog location={{query: ''}} params={{orgId: org.slug}} />, {
+      context: routerContext,
+    });
+
+    expect(await screen.findByText('Foo Bar (Sentry Staff)')).toBeInTheDocument();
+    expect(screen.getByText('Foo Bar')).toBeInTheDocument();
   });
 });

+ 2 - 2
tests/js/spec/views/settings/organizationAuditLog.spec.jsx

@@ -17,7 +17,7 @@ describe('OrganizationAuditLog', () => {
     ConfigStore.loadInitialData({user});
   });
 
-  it('renders', () => {
+  it('renders', async () => {
     MockApiClient.addMockResponse({
       url: `/organizations/org-slug/audit-logs/`,
       method: 'GET',
@@ -65,7 +65,7 @@ describe('OrganizationAuditLog', () => {
       }
     );
 
-    expect(screen.getByText('project.remove')).toBeInTheDocument();
+    expect(await screen.findByText('project.remove')).toBeInTheDocument();
     expect(screen.getByText('org.create')).toBeInTheDocument();
     expect(screen.getAllByText('127.0.0.1')).toHaveLength(2);
     expect(screen.getByText('17:29 PDT')).toBeInTheDocument();