Skip to content

Commit 67390ba

Browse files
isabellaenriquezpriscilawebdev
authored andcommitted
feat(sub v3): Update usage log component (#102676)
<img width="924" height="937" alt="Screenshot 2025-11-05 at 9 03 18 AM" src="https://github.com/user-attachments/assets/75c49c6f-5727-46e7-92d3-18177e5fa9ef" /> Loading state: <img width="1625" height="687" alt="Screenshot 2025-11-04 at 2 55 53 PM" src="https://github.com/user-attachments/assets/c3629333-195f-4c13-9726-b5b5d095de03" />
1 parent 5b57112 commit 67390ba

File tree

2 files changed

+101
-124
lines changed

2 files changed

+101
-124
lines changed

static/app/components/timeline/index.tsx

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import type {CSSProperties} from 'react';
22
import {useTheme, type Theme} from '@emotion/react';
33
import styled from '@emotion/styled';
44

5+
import {Flex} from '@sentry/scraps/layout';
6+
57
import {space} from 'sentry/styles/space';
68
import type {Color} from 'sentry/utils/theme';
79
import {isChonkTheme} from 'sentry/utils/theme/withChonk';
810

911
export interface TimelineItemProps {
10-
icon: React.ReactNode;
1112
title: React.ReactNode;
1213
children?: React.ReactNode;
1314
className?: string;
@@ -16,6 +17,7 @@ export interface TimelineItemProps {
1617
iconBorder: string | Color;
1718
title: string | Color;
1819
};
20+
icon?: React.ReactNode;
1921
isActive?: boolean;
2022
onClick?: React.MouseEventHandler<HTMLDivElement>;
2123
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
@@ -24,6 +26,7 @@ export interface TimelineItemProps {
2426
showLastLine?: boolean;
2527
style?: CSSProperties;
2628
timestamp?: React.ReactNode;
29+
titleTrailingItems?: React.ReactNode;
2730
}
2831

2932
function Item({
@@ -33,6 +36,7 @@ function Item({
3336
colorConfig,
3437
timestamp,
3538
isActive = false,
39+
titleTrailingItems,
3640
ref,
3741
...props
3842
}: TimelineItemProps) {
@@ -45,16 +49,23 @@ function Item({
4549

4650
return (
4751
<Row ref={ref} {...props}>
48-
<IconWrapper
49-
style={{
50-
borderColor: isActive ? iconBorder : 'transparent',
51-
color: iconColor,
52-
}}
53-
className="timeline-icon-wrapper"
54-
>
55-
{icon}
56-
</IconWrapper>
57-
<Title style={{color: titleColor}}>{title}</Title>
52+
{icon ? (
53+
<IconWrapper
54+
style={{
55+
borderColor: isActive ? iconBorder : 'transparent',
56+
color: iconColor,
57+
}}
58+
className="timeline-icon-wrapper"
59+
>
60+
{icon}
61+
</IconWrapper>
62+
) : (
63+
<IconWrapper className="timeline-icon-wrapper" />
64+
)}
65+
<Flex align="center" gap="xs" wrap="wrap">
66+
<Title style={{color: titleColor}}>{title}</Title>
67+
{titleTrailingItems}
68+
</Flex>
5869
{timestamp ?? <div />}
5970
<Spacer />
6071
<Content>{children}</Content>
Lines changed: 79 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import {Fragment} from 'react';
2-
import styled from '@emotion/styled';
2+
import {useTheme} from '@emotion/react';
33
import type {Location} from 'history';
44
import upperFirst from 'lodash/upperFirst';
55

6-
import {ActivityAvatar} from 'sentry/components/activity/item/avatar';
7-
import {UserAvatar} from 'sentry/components/core/avatar/userAvatar';
6+
import {Container, Flex, Grid} from '@sentry/scraps/layout';
7+
import {Text} from '@sentry/scraps/text';
8+
89
import {Tag} from 'sentry/components/core/badge/tag';
910
import {CompactSelect} from 'sentry/components/core/compactSelect';
1011
import {DateTime} from 'sentry/components/dateTime';
1112
import LoadingError from 'sentry/components/loadingError';
1213
import type {CursorHandler} from 'sentry/components/pagination';
1314
import Pagination from 'sentry/components/pagination';
14-
import {PanelTable} from 'sentry/components/panels/panelTable';
15+
import Placeholder from 'sentry/components/placeholder';
1516
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
17+
import {Timeline} from 'sentry/components/timeline';
18+
import {IconCircleFill} from 'sentry/icons';
1619
import {t} from 'sentry/locale';
17-
import {space} from 'sentry/styles/space';
1820
import type {AuditLog} from 'sentry/types/organization';
1921
import type {User} from 'sentry/types/user';
20-
import {shouldUse24Hours} from 'sentry/utils/dates';
22+
import {getTimeFormat} from 'sentry/utils/dates';
2123
import {useApiQuery} from 'sentry/utils/queryClient';
2224
import {decodeScalar} from 'sentry/utils/queryString';
2325
import {useMemoWithPrevious} from 'sentry/utils/useMemoWithPrevious';
@@ -33,38 +35,24 @@ import SubscriptionPageContainer from 'getsentry/views/subscriptionPage/componen
3335

3436
import SubscriptionHeader from './subscriptionHeader';
3537

36-
const avatarStyle = {
37-
width: 36,
38-
height: 36,
39-
marginRight: space(1),
40-
};
41-
42-
function LogAvatar({logEntryUser}: {logEntryUser: User | undefined}) {
43-
// Display Sentry's avatar for system or superuser-initiated events
44-
if (
45-
logEntryUser?.isSuperuser ||
46-
(logEntryUser?.name === 'Sentry' && logEntryUser?.email === undefined)
47-
) {
48-
return <SentryAvatar type="system" size={36} />;
49-
}
50-
// Display user's avatar for non-superusers-initiated events
51-
if (logEntryUser !== undefined) {
52-
return <UserAvatar style={avatarStyle} user={logEntryUser} />;
53-
}
54-
return null;
55-
}
56-
5738
function LogUsername({logEntryUser}: {logEntryUser: User | undefined}) {
5839
if (logEntryUser?.isSuperuser) {
5940
return (
60-
<StaffNote>
61-
{logEntryUser.name}
41+
<Flex align="center" gap="md">
42+
<Text variant="muted" size="sm">
43+
{logEntryUser.name}
44+
</Text>
6245
<Tag type="default">{t('Sentry Staff')}</Tag>
63-
</StaffNote>
46+
</Flex>
6447
);
6548
}
49+
6650
if (logEntryUser?.name !== 'Sentry' && logEntryUser !== undefined) {
67-
return <Note>{logEntryUser.name}</Note>;
51+
return (
52+
<Text variant="muted" size="sm">
53+
{logEntryUser.name}
54+
</Text>
55+
);
6856
}
6957
return null;
7058
}
@@ -95,9 +83,21 @@ type Props = {
9583
subscription: Subscription;
9684
};
9785

86+
function SkeletonEntry() {
87+
return (
88+
<Timeline.Item
89+
title={<Placeholder width="100px" height="20px" />}
90+
icon={<IconCircleFill />}
91+
>
92+
<Placeholder width="300px" height="36px" />
93+
</Timeline.Item>
94+
);
95+
}
96+
9897
function UsageLog({location, subscription}: Props) {
9998
const organization = useOrganization();
10099
const navigate = useNavigate();
100+
const theme = useTheme();
101101
const {
102102
data: auditLogs,
103103
isPending,
@@ -117,7 +117,6 @@ function UsageLog({location, subscription}: Props) {
117117
{staleTime: 0}
118118
);
119119

120-
//
121120
const eventNames = useMemoWithPrevious<string[] | null>(
122121
previous => auditLogs?.eventNames ?? previous,
123122
[auditLogs?.eventNames]
@@ -160,7 +159,7 @@ function UsageLog({location, subscription}: Props) {
160159

161160
const usageLogContent = (
162161
<Fragment>
163-
<UsageLogContainer>
162+
<Grid gap="2xl" flow="row">
164163
<CompactSelect
165164
searchable
166165
clearable
@@ -178,39 +177,55 @@ function UsageLog({location, subscription}: Props) {
178177
/>
179178
{isError ? (
180179
<LoadingError onRetry={refetch} />
180+
) : auditLogs?.rows?.length === 0 ? (
181+
<Text size="md">{t('No entries available.')}</Text>
181182
) : (
182-
<UsageTable
183-
headers={[t('Action'), t('Time')]}
184-
isEmpty={auditLogs?.rows && auditLogs?.rows.length === 0}
185-
emptyMessage={t('No entries available')}
186-
isLoading={isPending}
187-
>
188-
{auditLogs?.rows.map(entry => (
189-
<Fragment key={entry.id}>
190-
<UserInfo>
191-
<div>
192-
<LogAvatar logEntryUser={entry.actor} />
193-
</div>
194-
<NoteContainer>
195-
<LogUsername logEntryUser={entry.actor} />
196-
<Title>{formatEntryTitle(entry.event)}</Title>
197-
<Note>{formatEntryMessage(entry.note)}</Note>
198-
</NoteContainer>
199-
</UserInfo>
200-
201-
<TimestampInfo>
202-
<DateTime dateOnly date={entry.dateCreated} />
203-
<DateTime
204-
timeOnly
205-
format={shouldUse24Hours() ? 'HH:mm zz' : 'LT zz'}
206-
date={entry.dateCreated}
207-
/>
208-
</TimestampInfo>
209-
</Fragment>
210-
))}
211-
</UsageTable>
183+
<Timeline.Container>
184+
{isPending
185+
? Array.from({length: 50}).map((_, index) => <SkeletonEntry key={index} />)
186+
: auditLogs?.rows.map((entry, index) => (
187+
<Timeline.Item
188+
key={entry.id}
189+
colorConfig={{
190+
icon: index === 0 ? theme.active : theme.gray300,
191+
iconBorder: index === 0 ? theme.active : theme.gray300,
192+
title: theme.textColor,
193+
}}
194+
icon={<IconCircleFill />}
195+
title={formatEntryTitle(entry.event)}
196+
titleTrailingItems={
197+
<Fragment>
198+
<Text size="md" variant="muted" bold>
199+
{' ・ '}
200+
</Text>
201+
<Grid columns="max-content auto" gap="md">
202+
<DateTime
203+
format={`MMM D, YYYY ・ ${getTimeFormat({timeZone: true})}`}
204+
date={entry.dateCreated}
205+
style={{fontSize: theme.fontSize.sm}}
206+
/>
207+
</Grid>
208+
{entry.actor && entry.actor.name !== 'Sentry' && (
209+
<Fragment>
210+
<Text size="sm" variant="muted" bold>
211+
{' ・ '}
212+
</Text>
213+
<LogUsername logEntryUser={entry.actor} />
214+
</Fragment>
215+
)}
216+
</Fragment>
217+
}
218+
>
219+
<Container paddingBottom="xl" maxWidth="800px">
220+
<Text variant="muted" size="md">
221+
{formatEntryMessage(entry.note)}
222+
</Text>
223+
</Container>
224+
</Timeline.Item>
225+
))}
226+
</Timeline.Container>
212227
)}
213-
</UsageLogContainer>
228+
</Grid>
214229
<Pagination pageLinks={getResponseHeader?.('Link')} onCursor={handleCursor} />
215230
</Fragment>
216231
);
@@ -235,52 +250,3 @@ function UsageLog({location, subscription}: Props) {
235250

236251
export default withSubscription(UsageLog);
237252
export {UsageLog};
238-
239-
const SentryAvatar = styled(ActivityAvatar)`
240-
margin-right: ${space(1)};
241-
`;
242-
243-
const Note = styled('div')`
244-
font-size: ${p => p.theme.fontSize.md};
245-
word-break: break-word;
246-
`;
247-
248-
const StaffNote = styled(Note)`
249-
display: flex;
250-
gap: ${space(1)};
251-
line-height: 1.5;
252-
`;
253-
254-
const UsageLogContainer = styled('div')`
255-
display: grid;
256-
grid-auto-flow: row;
257-
gap: ${space(3)};
258-
`;
259-
260-
const UsageTable = styled(PanelTable)`
261-
box-shadow: inset 0px -1px 0px ${p => p.theme.gray200};
262-
`;
263-
264-
const UserInfo = styled('div')`
265-
font-size: ${p => p.theme.fontSize.sm};
266-
min-width: 250px;
267-
display: flex;
268-
`;
269-
270-
const NoteContainer = styled('div')`
271-
display: flex;
272-
flex-direction: column;
273-
justify-content: center;
274-
`;
275-
276-
const Title = styled('div')`
277-
font-size: ${p => p.theme.fontSize.lg};
278-
`;
279-
280-
const TimestampInfo = styled('div')`
281-
display: grid;
282-
grid-template-columns: max-content auto;
283-
gap: ${space(1)};
284-
font-size: ${p => p.theme.fontSize.md};
285-
align-content: center;
286-
`;

0 commit comments

Comments
 (0)