11import { Fragment } from 'react' ;
2- import styled from '@emotion/styled ' ;
2+ import { useTheme } from '@emotion/react ' ;
33import type { Location } from 'history' ;
44import 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+
89import { Tag } from 'sentry/components/core/badge/tag' ;
910import { CompactSelect } from 'sentry/components/core/compactSelect' ;
1011import { DateTime } from 'sentry/components/dateTime' ;
1112import LoadingError from 'sentry/components/loadingError' ;
1213import type { CursorHandler } from 'sentry/components/pagination' ;
1314import Pagination from 'sentry/components/pagination' ;
14- import { PanelTable } from 'sentry/components/panels/panelTable ' ;
15+ import Placeholder from 'sentry/components/placeholder ' ;
1516import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle' ;
17+ import { Timeline } from 'sentry/components/timeline' ;
18+ import { IconCircleFill } from 'sentry/icons' ;
1619import { t } from 'sentry/locale' ;
17- import { space } from 'sentry/styles/space' ;
1820import type { AuditLog } from 'sentry/types/organization' ;
1921import type { User } from 'sentry/types/user' ;
20- import { shouldUse24Hours } from 'sentry/utils/dates' ;
22+ import { getTimeFormat } from 'sentry/utils/dates' ;
2123import { useApiQuery } from 'sentry/utils/queryClient' ;
2224import { decodeScalar } from 'sentry/utils/queryString' ;
2325import { useMemoWithPrevious } from 'sentry/utils/useMemoWithPrevious' ;
@@ -33,38 +35,24 @@ import SubscriptionPageContainer from 'getsentry/views/subscriptionPage/componen
3335
3436import 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-
5738function 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+
9897function 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
236251export default withSubscription ( UsageLog ) ;
237252export { 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