UserLogs.tsx194 lines · main
| 1 | import { useParams } from 'common' |
| 2 | import { ExternalLink, RefreshCw } from 'lucide-react' |
| 3 | import Link from 'next/link' |
| 4 | import { useQueryState } from 'nuqs' |
| 5 | import { useEffect } from 'react' |
| 6 | import { Button, cn, CriticalIcon, Separator } from 'ui' |
| 7 | import { Admonition, TimestampInfo } from 'ui-patterns' |
| 8 | import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' |
| 9 | |
| 10 | import { UserHeader } from './UserHeader' |
| 11 | import { PANEL_PADDING } from './Users.constants' |
| 12 | import { LOGS_TABLES } from '@/components/interfaces/Settings/Logs/Logs.constants' |
| 13 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 14 | import { User } from '@/data/auth/users-infinite-query' |
| 15 | import useLogsPreview from '@/hooks/analytics/useLogsPreview' |
| 16 | import { useLogsUrlState } from '@/hooks/analytics/useLogsUrlState' |
| 17 | |
| 18 | interface UserLogsProps { |
| 19 | user: User |
| 20 | } |
| 21 | |
| 22 | const 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 | |
| 25 | export 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 | } |