|
@@ -1,15 +1,17 @@
|
|
|
+import {Fragment} from 'react';
|
|
|
import styled from '@emotion/styled';
|
|
|
|
|
|
import UserAvatar from 'app/components/avatar/userAvatar';
|
|
|
import DateTime from 'app/components/dateTime';
|
|
|
import SelectField from 'app/components/forms/selectField';
|
|
|
import Pagination from 'app/components/pagination';
|
|
|
-import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
|
|
|
+import {PanelTable} from 'app/components/panels';
|
|
|
import Tooltip from 'app/components/tooltip';
|
|
|
import {t} from 'app/locale';
|
|
|
import overflowEllipsis from 'app/styles/overflowEllipsis';
|
|
|
import space from 'app/styles/space';
|
|
|
-import EmptyMessage from 'app/views/settings/components/emptyMessage';
|
|
|
+import {AuditLog} from 'app/types';
|
|
|
+import {use24Hours} from 'app/utils/dates';
|
|
|
import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader';
|
|
|
|
|
|
const avatarStyle = {
|
|
@@ -19,20 +21,23 @@ const avatarStyle = {
|
|
|
};
|
|
|
|
|
|
type Props = {
|
|
|
- entries: any[];
|
|
|
- pageLinks: string;
|
|
|
+ isLoading: boolean;
|
|
|
+ entries: AuditLog[] | null;
|
|
|
+ pageLinks: string | null;
|
|
|
eventType: string;
|
|
|
eventTypes: string[];
|
|
|
onEventSelect: (value: string) => void;
|
|
|
};
|
|
|
|
|
|
const AuditLogList = ({
|
|
|
+ isLoading,
|
|
|
pageLinks,
|
|
|
entries,
|
|
|
eventType,
|
|
|
eventTypes,
|
|
|
onEventSelect,
|
|
|
}: Props) => {
|
|
|
+ const is24Hours = use24Hours();
|
|
|
const hasEntries = entries && entries.length > 0;
|
|
|
const ipv4Length = 15;
|
|
|
const options = [
|
|
@@ -55,54 +60,55 @@ const AuditLogList = ({
|
|
|
return (
|
|
|
<div>
|
|
|
<SettingsPageHeader title={t('Audit Log')} action={action} />
|
|
|
- <Panel>
|
|
|
- <StyledPanelHeader disablePadding>
|
|
|
- <div>{t('Member')}</div>
|
|
|
- <div>{t('Action')}</div>
|
|
|
- <div>{t('IP')}</div>
|
|
|
- <div>{t('Time')}</div>
|
|
|
- </StyledPanelHeader>
|
|
|
-
|
|
|
- <PanelBody>
|
|
|
- {!hasEntries && <EmptyMessage>{t('No audit entries available')}</EmptyMessage>}
|
|
|
-
|
|
|
- {hasEntries &&
|
|
|
- entries.map(entry => (
|
|
|
- <StyledPanelItem center key={entry.id}>
|
|
|
- <UserInfo>
|
|
|
- <div>
|
|
|
- {entry.actor.email && (
|
|
|
- <UserAvatar style={avatarStyle} user={entry.actor} />
|
|
|
- )}
|
|
|
- </div>
|
|
|
- <NameContainer>
|
|
|
- <Name data-test-id="actor-name">
|
|
|
- {entry.actor.isSuperuser
|
|
|
- ? t('%s (Sentry Staff)', entry.actor.name)
|
|
|
- : entry.actor.name}
|
|
|
- </Name>
|
|
|
- <Note>{entry.note}</Note>
|
|
|
- </NameContainer>
|
|
|
- </UserInfo>
|
|
|
- <div>
|
|
|
- <MonoDetail>{entry.event}</MonoDetail>
|
|
|
- </div>
|
|
|
- <TimestampOverflow>
|
|
|
+ <PanelTable
|
|
|
+ headers={[t('Member'), t('Action'), t('IP'), t('Time')]}
|
|
|
+ isEmpty={!hasEntries}
|
|
|
+ emptyMessage={t('No audit entries available')}
|
|
|
+ isLoading={isLoading}
|
|
|
+ >
|
|
|
+ {entries?.map(entry => (
|
|
|
+ <Fragment key={entry.id}>
|
|
|
+ <UserInfo>
|
|
|
+ <div>
|
|
|
+ {entry.actor.email && (
|
|
|
+ <UserAvatar style={avatarStyle} user={entry.actor} />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <NameContainer>
|
|
|
+ <Name data-test-id="actor-name">
|
|
|
+ {entry.actor.isSuperuser
|
|
|
+ ? t('%s (Sentry Staff)', entry.actor.name)
|
|
|
+ : entry.actor.name}
|
|
|
+ </Name>
|
|
|
+ <Note>{entry.note}</Note>
|
|
|
+ </NameContainer>
|
|
|
+ </UserInfo>
|
|
|
+ <FlexCenter>
|
|
|
+ <MonoDetail>{entry.event}</MonoDetail>
|
|
|
+ </FlexCenter>
|
|
|
+ <FlexCenter>
|
|
|
+ {entry.ipAddress && (
|
|
|
+ <IpAddressOverflow>
|
|
|
<Tooltip
|
|
|
title={entry.ipAddress}
|
|
|
- disabled={entry.ipAddress && entry.ipAddress.length <= ipv4Length}
|
|
|
+ disabled={entry.ipAddress.length <= ipv4Length}
|
|
|
>
|
|
|
<MonoDetail>{entry.ipAddress}</MonoDetail>
|
|
|
</Tooltip>
|
|
|
- </TimestampOverflow>
|
|
|
- <TimestampInfo>
|
|
|
- <DateTime dateOnly date={entry.dateCreated} />
|
|
|
- <DateTime timeOnly format="LT zz" date={entry.dateCreated} />
|
|
|
- </TimestampInfo>
|
|
|
- </StyledPanelItem>
|
|
|
- ))}
|
|
|
- </PanelBody>
|
|
|
- </Panel>
|
|
|
+ </IpAddressOverflow>
|
|
|
+ )}
|
|
|
+ </FlexCenter>
|
|
|
+ <TimestampInfo>
|
|
|
+ <DateTime dateOnly date={entry.dateCreated} />
|
|
|
+ <DateTime
|
|
|
+ timeOnly
|
|
|
+ format={is24Hours ? 'HH:mm zz' : 'LT zz'}
|
|
|
+ date={entry.dateCreated}
|
|
|
+ />
|
|
|
+ </TimestampInfo>
|
|
|
+ </Fragment>
|
|
|
+ ))}
|
|
|
+ </PanelTable>
|
|
|
{pageLinks && <Pagination pageLinks={pageLinks} />}
|
|
|
</div>
|
|
|
);
|
|
@@ -110,9 +116,10 @@ const AuditLogList = ({
|
|
|
|
|
|
const UserInfo = styled('div')`
|
|
|
display: flex;
|
|
|
+ align-items: center;
|
|
|
line-height: 1.2;
|
|
|
font-size: 13px;
|
|
|
- flex: 1;
|
|
|
+ min-width: 250px;
|
|
|
`;
|
|
|
|
|
|
const NameContainer = styled('div')`
|
|
@@ -125,30 +132,25 @@ const Name = styled('div')`
|
|
|
font-weight: 600;
|
|
|
font-size: 15px;
|
|
|
`;
|
|
|
+
|
|
|
const Note = styled('div')`
|
|
|
font-size: 13px;
|
|
|
word-break: break-word;
|
|
|
`;
|
|
|
-const TimestampOverflow = styled('div')`
|
|
|
- ${overflowEllipsis};
|
|
|
-`;
|
|
|
|
|
|
-const MonoDetail = styled('code')`
|
|
|
- font-size: ${p => p.theme.fontSizeMedium};
|
|
|
+const FlexCenter = styled('div')`
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
`;
|
|
|
|
|
|
-const StyledPanelHeader = styled(PanelHeader)`
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 1fr max-content 130px 150px;
|
|
|
- grid-column-gap: ${space(2)};
|
|
|
- padding: ${space(2)};
|
|
|
+const IpAddressOverflow = styled('div')`
|
|
|
+ ${overflowEllipsis};
|
|
|
+ min-width: 90px;
|
|
|
`;
|
|
|
|
|
|
-const StyledPanelItem = styled(PanelItem)`
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 1fr max-content 130px 150px;
|
|
|
- grid-column-gap: ${space(2)};
|
|
|
- padding: ${space(2)};
|
|
|
+const MonoDetail = styled('code')`
|
|
|
+ font-size: ${p => p.theme.fontSizeMedium};
|
|
|
+ white-space: no-wrap;
|
|
|
`;
|
|
|
|
|
|
const TimestampInfo = styled('div')`
|