Browse Source

feat(auditlog): Update Audit Log UI to use endpoint version 2 (#36494)

* feat(auditlog): Update UI to use endpoint version 2

* Update tests

* Add Sentry avatar and staff tag

* Fix test

* Update font sizes to themes
Maggie Bauer 2 years ago
parent
commit
096aa2e10e

+ 40 - 0
fixtures/js-stubs/auditLogsApiEventNames.js

@@ -0,0 +1,40 @@
+export function AuditLogsApiEventNames(params = []) {
+  return [
+    ...params,
+    'member.invite',
+    'member.add',
+    'member.accept-invite',
+    'member.remove',
+    'member.edit',
+    'member.join-team',
+    'member.leave-team',
+    'member.pending',
+    'team.create',
+    'team.edit',
+    'team.remove',
+    'project.create',
+    'project.edit',
+    'project.remove',
+    'project.set-public',
+    'project.set-private',
+    'project.request-transfer',
+    'project.accept-transfer',
+    'org.create',
+    'org.edit',
+    'org.remove',
+    'org.restore',
+    'sso.enable',
+    'sso.disable',
+    'sso.edit',
+    'sso-identity.link',
+    'alertrule.create',
+    'alertrule.edit',
+    'alertrule.remove',
+    'rule.create',
+    'rule.edit',
+    'rule.remove',
+    'integration.add',
+    'integration.edit',
+    'integration.remove',
+  ];
+}

+ 51 - 20
static/app/views/settings/organizationAuditLog/auditLogList.tsx

@@ -1,15 +1,17 @@
 import {Fragment} from 'react';
 import styled from '@emotion/styled';
 
+import ActivityAvatar from 'sentry/components/activity/item/avatar';
 import UserAvatar from 'sentry/components/avatar/userAvatar';
 import DateTime from 'sentry/components/dateTime';
 import SelectControl from 'sentry/components/forms/selectControl';
 import Pagination, {CursorHandler} from 'sentry/components/pagination';
 import {PanelTable} from 'sentry/components/panels';
+import Tag from 'sentry/components/tag';
 import Tooltip from 'sentry/components/tooltip';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
-import {AuditLog} from 'sentry/types';
+import {AuditLog, User} from 'sentry/types';
 import {shouldUse24Hours} from 'sentry/utils/dates';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 
@@ -19,10 +21,40 @@ const avatarStyle = {
   marginRight: 8,
 };
 
+const getAvatarDisplay = (logEntryUser: User | undefined) => {
+  // Display Sentry's avatar for system or superuser-initiated events
+  if (
+    logEntryUser?.isSuperuser ||
+    (logEntryUser?.name === 'Sentry' && logEntryUser?.email === undefined)
+  ) {
+    return <SentryAvatar type="system" size={36} />;
+  }
+  // Display user's avatar for non-superusers-initiated events
+  if (logEntryUser !== undefined) {
+    return <UserAvatar style={avatarStyle} user={logEntryUser} />;
+  }
+  return null;
+};
+
+const addUsernameDisplay = (logEntryUser: User | undefined) => {
+  if (logEntryUser?.isSuperuser) {
+    return (
+      <Name data-test-id="actor-name">
+        {logEntryUser.name}
+        <StaffTag>{t('Sentry Staff')}</StaffTag>
+      </Name>
+    );
+  }
+  if (logEntryUser !== undefined) {
+    return <Name data-test-id="actor-name">{logEntryUser.name}</Name>;
+  }
+  return null;
+};
+
 type Props = {
   entries: AuditLog[] | null;
   eventType: string | undefined;
-  eventTypes: string[];
+  eventTypes: string[] | null;
   isLoading: boolean;
   onCursor: CursorHandler | undefined;
   onEventSelect: (value: string) => void;
@@ -41,7 +73,7 @@ const AuditLogList = ({
   const hasEntries = entries && entries.length > 0;
   const ipv4Length = 15;
 
-  const eventOptions = eventTypes.map(type => ({
+  const eventOptions = eventTypes?.map(type => ({
     label: type,
     value: type,
   }));
@@ -71,17 +103,9 @@ const AuditLogList = ({
         {entries?.map(entry => (
           <Fragment key={entry.id}>
             <UserInfo>
-              <div>
-                {entry.actor.email && (
-                  <UserAvatar style={avatarStyle} user={entry.actor} />
-                )}
-              </div>
+              <div>{getAvatarDisplay(entry.actor)}</div>
               <NameContainer>
-                <Name data-test-id="actor-name">
-                  {entry.actor.isSuperuser
-                    ? t('%s (Sentry Staff)', entry.actor.name)
-                    : entry.actor.name}
-                </Name>
+                {addUsernameDisplay(entry.actor)}
                 <Note>{entry.note}</Note>
               </NameContainer>
             </UserInfo>
@@ -116,6 +140,18 @@ const AuditLogList = ({
   );
 };
 
+const SentryAvatar = styled(ActivityAvatar)`
+  margin-right: ${space(1)};
+`;
+
+const Name = styled('strong')`
+  font-size: ${p => p.theme.fontSizeMedium};
+`;
+
+const StaffTag = styled(Tag)`
+  padding: ${space(1)};
+`;
+
 const EventSelector = styled(SelectControl)`
   width: 250px;
 `;
@@ -124,7 +160,7 @@ const UserInfo = styled('div')`
   display: flex;
   align-items: center;
   line-height: 1.2;
-  font-size: 13px;
+  font-size: ${p => p.theme.fontSizeSmall};
   min-width: 250px;
 `;
 
@@ -134,13 +170,8 @@ const NameContainer = styled('div')`
   justify-content: center;
 `;
 
-const Name = styled('div')`
-  font-weight: 600;
-  font-size: 15px;
-`;
-
 const Note = styled('div')`
-  font-size: 13px;
+  font-size: ${p => p.theme.fontSizeSmall};
   word-break: break-word;
 `;
 

+ 6 - 64
static/app/views/settings/organizationAuditLog/index.tsx

@@ -9,67 +9,6 @@ import withOrganization from 'sentry/utils/withOrganization';
 
 import AuditLogList from './auditLogList';
 
-// Please keep this list sorted
-const EVENT_TYPES = [
-  'member.invite',
-  'member.add',
-  'member.accept-invite',
-  'member.remove',
-  'member.edit',
-  'member.join-team',
-  'member.leave-team',
-  'member.pending',
-  'team.create',
-  'team.edit',
-  'team.remove',
-  'project.create',
-  'project.edit',
-  'project.remove',
-  'project.set-public',
-  'project.set-private',
-  'project.request-transfer',
-  'project.accept-transfer',
-  'org.create',
-  'org.edit',
-  'org.remove',
-  'org.restore',
-  'tagkey.remove',
-  'projectkey.create',
-  'projectkey.edit',
-  'projectkey.remove',
-  'projectkey.enable',
-  'projectkey.disable',
-  'sso.enable',
-  'sso.disable',
-  'sso.edit',
-  'sso-identity.link',
-  'api-key.create',
-  'api-key.edit',
-  'api-key.remove',
-  'alertrule.create',
-  'alertrule.edit',
-  'alertrule.remove',
-  'rule.create',
-  'rule.edit',
-  'rule.remove',
-  'servicehook.create',
-  'servicehook.edit',
-  'servicehook.remove',
-  'servicehook.enable',
-  'servicehook.disable',
-  'integration.add',
-  'integration.edit',
-  'integration.remove',
-  'ondemand.edit',
-  'trial.started',
-  'plan.changed',
-  'plan.cancelled',
-  'sentry-app.add',
-  'sentry-app.remove',
-  'sentry-app.install',
-  'sentry-app.uninstall',
-];
-
 type Props = {
   organization: Organization;
 };
@@ -77,6 +16,7 @@ type Props = {
 type State = {
   entryList: AuditLog[] | null;
   entryListPageLinks: string | null;
+  eventTypes: string[] | null;
   isLoading: boolean;
   currentCursor?: string;
   eventType?: string;
@@ -86,6 +26,7 @@ function OrganizationAuditLog({organization}: Props) {
   const [state, setState] = useState<State>({
     entryList: [],
     entryListPageLinks: null,
+    eventTypes: [],
     isLoading: true,
   });
 
@@ -100,7 +41,7 @@ function OrganizationAuditLog({organization}: Props) {
 
   const fetchAuditLogData = useCallback(async () => {
     try {
-      const payload = {cursor: state.currentCursor, event: state.eventType};
+      const payload = {cursor: state.currentCursor, event: state.eventType, version: '2'};
       if (!payload.cursor) {
         delete payload.cursor;
       }
@@ -117,7 +58,8 @@ function OrganizationAuditLog({organization}: Props) {
       );
       setState(prevState => ({
         ...prevState,
-        entryList: data,
+        entryList: data.rows,
+        eventTypes: data.options.sort(),
         isLoading: false,
         entryListPageLinks: response?.getResponseHeader('Link') ?? null,
       }));
@@ -150,7 +92,7 @@ function OrganizationAuditLog({organization}: Props) {
         entries={state.entryList}
         pageLinks={state.entryListPageLinks}
         eventType={state.eventType}
-        eventTypes={EVENT_TYPES}
+        eventTypes={state.eventTypes}
         onEventSelect={handleEventSelect}
         isLoading={state.isLoading}
         onCursor={handleCursor}

+ 32 - 14
tests/js/spec/views/settings/auditLogView.spec.jsx

@@ -8,7 +8,7 @@ describe('OrganizationAuditLog', () => {
   const {routerContext, org} = initializeOrg({
     projects: [],
     router: {
-      location: {query: {}},
+      location: {query: {version: '2'}},
       params: {orgId: 'org-slug'},
     },
   });
@@ -18,14 +18,20 @@ describe('OrganizationAuditLog', () => {
     Client.clearMockResponses();
     Client.addMockResponse({
       url: ENDPOINT,
-      body: TestStubs.AuditLogs(),
+      body: {rows: TestStubs.AuditLogs(), options: TestStubs.AuditLogsApiEventNames()},
     });
   });
 
   it('renders', async () => {
-    render(<OrganizationAuditLog location={{query: ''}} params={{orgId: org.slug}} />, {
-      context: routerContext,
-    });
+    render(
+      <OrganizationAuditLog
+        location={{query: {version: '2'}}}
+        params={{orgId: org.slug}}
+      />,
+      {
+        context: routerContext,
+      }
+    );
 
     expect(await screen.findByRole('heading')).toHaveTextContent('Audit Log');
     expect(screen.getByRole('textbox')).toBeInTheDocument();
@@ -41,22 +47,34 @@ describe('OrganizationAuditLog', () => {
     Client.clearMockResponses();
     Client.addMockResponse({
       url: ENDPOINT,
-      body: [],
+      body: {rows: [], options: TestStubs.AuditLogsApiEventNames()},
     });
 
-    render(<OrganizationAuditLog location={{query: ''}} params={{orgId: org.slug}} />, {
-      context: routerContext,
-    });
+    render(
+      <OrganizationAuditLog
+        location={{query: {version: '2'}}}
+        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,
-    });
+    render(
+      <OrganizationAuditLog
+        location={{query: {version: '2'}}}
+        params={{orgId: org.slug}}
+      />,
+      {
+        context: routerContext,
+      }
+    );
 
-    expect(await screen.findByText('Foo Bar (Sentry Staff)')).toBeInTheDocument();
-    expect(screen.getByText('Foo Bar')).toBeInTheDocument();
+    expect(await screen.findByText('Sentry Staff')).toBeInTheDocument();
+    expect(screen.getAllByText('Foo Bar')).toHaveLength(2);
   });
 });

+ 28 - 25
tests/js/spec/views/settings/organizationAuditLog.spec.jsx

@@ -21,36 +21,39 @@ describe('OrganizationAuditLog', () => {
     MockApiClient.addMockResponse({
       url: `/organizations/org-slug/audit-logs/`,
       method: 'GET',
-      body: [
-        {
-          id: '4500000',
-          actor: TestStubs.User(),
-          event: 'project.remove',
-          ipAddress: '127.0.0.1',
-          note: 'removed project test',
-          targetObject: 5466660,
-          targetUser: null,
-          data: {},
-          dateCreated: '2021-09-28T00:29:33.940848Z',
-        },
-        {
-          id: '430000',
-          actor: TestStubs.User(),
-          event: 'org.create',
-          ipAddress: '127.0.0.1',
-          note: 'created the organization',
-          targetObject: 54215,
-          targetUser: null,
-          data: {},
-          dateCreated: '2016-11-21T04:02:45.929313Z',
-        },
-      ],
+      body: {
+        rows: [
+          {
+            id: '4500000',
+            actor: TestStubs.User(),
+            event: 'project.remove',
+            ipAddress: '127.0.0.1',
+            note: 'removed project test',
+            targetObject: 5466660,
+            targetUser: null,
+            data: {},
+            dateCreated: '2021-09-28T00:29:33.940848Z',
+          },
+          {
+            id: '430000',
+            actor: TestStubs.User(),
+            event: 'org.create',
+            ipAddress: '127.0.0.1',
+            note: 'created the organization',
+            targetObject: 54215,
+            targetUser: null,
+            data: {},
+            dateCreated: '2016-11-21T04:02:45.929313Z',
+          },
+        ],
+        options: TestStubs.AuditLogsApiEventNames(),
+      },
     });
 
     const {router, routerContext, organization} = initializeOrg({
       projects: [],
       router: {
-        location: {query: {}},
+        location: {query: {version: '2'}},
         params: {orgId: 'org-slug'},
       },
     });