UserLogs.tsx194 lines · main
1import { useParams } from 'common'
2import { ExternalLink, RefreshCw } from 'lucide-react'
3import Link from 'next/link'
4import { useQueryState } from 'nuqs'
5import { useEffect } from 'react'
6import { Button, cn, CriticalIcon, Separator } from 'ui'
7import { Admonition, TimestampInfo } from 'ui-patterns'
8import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
9
10import { UserHeader } from './UserHeader'
11import { PANEL_PADDING } from './Users.constants'
12import { LOGS_TABLES } from '@/components/interfaces/Settings/Logs/Logs.constants'
13import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
14import { User } from '@/data/auth/users-infinite-query'
15import useLogsPreview from '@/hooks/analytics/useLogsPreview'
16import { useLogsUrlState } from '@/hooks/analytics/useLogsUrlState'
17
18interface UserLogsProps {
19 user: User
20}
21
22const API_LOGS_QUERY = (userId: string) =>
23 `select\n cast(timestamp as datetime) as timestamp,\n event_message, metadata \nfrom edge_logs \nWHERE (\n metadata[SAFE_OFFSET(0)].request[SAFE_OFFSET(0)].sb[SAFE_OFFSET(0)].auth_user\n = '${userId}'\n)\nlimit 100`
24
25export const UserLogs = ({ user }: UserLogsProps) => {
26 const { ref } = useParams()
27 const { filters, setFilters } = useLogsUrlState()
28 const [, setFiltersValue] = useQueryState('f')
29
30 const {
31 logData: authLogs,
32 isSuccess: isSuccessAuthLogs,
33 isLoading: isLoadingAuthLogs,
34 refresh,
35 } = useLogsPreview({
36 projectRef: ref as string,
37 table: LOGS_TABLES.auth,
38 filterOverride: { search_query: user.id },
39 limit: 5,
40 })
41
42 useEffect(() => {
43 if (user.id) setFilters({ ...filters, search_query: user.id })
44
45 return () => {
46 setFiltersValue(null)
47 }
48 // eslint-disable-next-line react-hooks/exhaustive-deps
49 }, [user.id])
50
51 return (
52 <div>
53 <UserHeader user={user} />
54
55 <Separator />
56
57 <div className={cn('flex flex-col gap-y-3', PANEL_PADDING)}>
58 <div>
59 <p>API logs</p>
60 <p className="text-sm text-foreground-light">
61 View edge logs for requests made by this user
62 </p>
63 </div>
64
65 <Button asChild type="default" className="w-min">
66 <Link
67 href={`/project/${ref}/logs/explorer?q=${encodeURIComponent(API_LOGS_QUERY(user.id ?? ''))}`}
68 >
69 Open in Log Explorer
70 </Link>
71 </Button>
72 </div>
73
74 <Separator />
75
76 <div className={cn('flex flex-col gap-y-3', PANEL_PADDING)}>
77 <div>
78 <p>Authentication logs</p>
79 <p className="text-sm text-foreground-light">
80 Latest logs from authentication for this user in the past hour
81 </p>
82 </div>
83
84 {/* [Joshen] This whole thing here i reckon we can shift to a component, if in the future we wanna add more user logs */}
85 <div className="flex items-center justify-between">
86 <div className="flex items-center">
87 <Button
88 type={'status_code' in filters ? 'default' : 'secondary'}
89 className="rounded-r-none border-r-0"
90 disabled={isLoadingAuthLogs}
91 onClick={() => setFilters({ search_query: user.id })}
92 >
93 Show all
94 </Button>
95 <div className="border-button border border-l-0 py-3" />
96 <Button
97 type={'status_code' in filters ? 'secondary' : 'default'}
98 className="rounded-l-none border-l-0"
99 disabled={isLoadingAuthLogs}
100 onClick={() =>
101 setFilters({
102 search_query: user.id,
103 status_code: { client_error: true, server_error: true },
104 })
105 }
106 >
107 Error only
108 </Button>
109 </div>
110 <Button
111 type="default"
112 loading={isLoadingAuthLogs}
113 disabled={isLoadingAuthLogs}
114 icon={<RefreshCw />}
115 onClick={() => refresh()}
116 >
117 Refresh
118 </Button>
119 </div>
120
121 {isLoadingAuthLogs && !isSuccessAuthLogs ? (
122 <GenericSkeletonLoader />
123 ) : authLogs.length === 0 ? (
124 <Admonition
125 type="note"
126 title="No authentication logs available for this user"
127 description="Auth events such as logging in will be shown here"
128 />
129 ) : (
130 <div>
131 <div className="border border-b-0 rounded-t-md divide-y overflow-hidden">
132 {authLogs.map((log) => {
133 const status = ((log.status ?? '-') as any).toString()
134 const is400 = status.startsWith('4')
135 const is500 = status.startsWith('5')
136
137 return (
138 <div
139 key={log.id}
140 className="flex items-center transition font-mono px-2 py-1.5 bg-surface-100 divide-x"
141 >
142 <p className="text-xs text-foreground-light min-w-[125px] w-[125px] px-1">
143 <TimestampInfo utcTimestamp={log.timestamp / 1000} />
144 </p>
145 <div className="flex items-center text-xs text-foreground-light h-[22px] min-w-[70px] w-[70px] px-2">
146 <div
147 className={cn(
148 'flex items-center justify-center gap-x-1',
149 !!log.status && 'border px-1 py-0.5 rounded-sm',
150 is400
151 ? 'text-warning border-warning bg-warning-300'
152 : is500
153 ? 'text-destructive border-destructive bg-destructive-300'
154 : ''
155 )}
156 >
157 {(is400 || is500) && (
158 <CriticalIcon hideBackground className={cn(is400 && 'text-warning')} />
159 )}
160 {status}
161 </div>
162 </div>
163 <p className="group relative flex items-center py-1.5 text-xs text-foreground-light px-2 truncate w-full">
164 {`${log.path} | ${log.msg}`}
165
166 <ButtonTooltip
167 type="outline"
168 asChild
169 tooltip={{ content: { text: 'Open in logs' } }}
170 className="px-1.5 absolute right-0 top-0 opacity-0 group-hover:opacity-100 transition bg-background focus-visible:opacity-100"
171 >
172 <Link href={`/project/${ref}/logs/auth-logs?log=${log.id}`}>
173 <ExternalLink size="12" className="text-foreground-light" />
174 </Link>
175 </ButtonTooltip>
176 </p>
177 </div>
178 )
179 })}
180 </div>
181 <Button
182 block
183 asChild
184 type="outline"
185 className="transition rounded-t-none text-foreground-light hover:text-foreground"
186 >
187 <Link href={`/project/${ref}/logs/auth-logs?s=${user.id}`}>See more logs</Link>
188 </Button>
189 </div>
190 )}
191 </div>
192 </div>
193 )
194}