Ghost

struxadotcloud/struxa

public
Fork0
CodeIssues0Pull requests0
1branch
struxaappswebsrcapp(admin)adminactivitypage.tsx
fix(admin): monitoring review fixesc53ad032 months ago245 lines · 11.4 KB
1"use client";
2
3import { useEffect } from
4import { useRouter }
5import { useQuery }
6import { useTranslations }
7import { Activity, Server
8import type { LucideIcon
9import { orpc }
10import { authClient }
11import Loader from "@/components/loader
12import { DitherAvatar }
13
14function StatRow({
15 icon: Icon,
16 label,
17 children,
18}: {
19 icon: LucideIcon
20 label: string
21 children: React
22}) {
23 return (
24 <div className="flex flex-col border-b border-border last:border-b-0
25 <div className="flex flex-col gap-1.5 px-4 pt-3 pb-2.5
26 <div className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground
27 <Icon className="
28 {label}
29 </div>
30 {children}
31 </div>
32 </div>
33 );
34}
35
36const EVENT_STYLES: Record
37 server: { color: "
38 user: { color: "
39 node: { color: "
40 alloc: { color: "
41 nest: { color: "
42 egg: { color: "
43 dbhost: { color: "
44 location: { color: "
45 settings: { color: "
46 billing: { color: "
47 backup: { color: "
48};
49
50function getEventStyle(event:
51 if (event.startsWith
52 if (event.startsWith
53 if (event.startsWith
54 if (event.startsWith
55 if (event.startsWith
56 if (event.startsWith
57 if (event.startsWith
58 if (event.startsWith
59 if (event.startsWith
60 if (event.startsWith
61 if (event.startsWith
62 return { color:
63}
64
65const prettify = (
66 slug.replace(/[
67
68function useEventLabel() {
69 const t =
70 return (event:
71 const path =
72 const dot =
73 if (dot <
74 const part =
75 t.has(`${
76 return `${part(
77 };
78}
79
80function ActorCell({ user
81 const t =
82 if (!user)
83 const displayName =
84 return (
85 <div className="flex items-center gap-2 min-w-0
86 <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-semibold text-foreground overflow-hidden
87 {user.image ?
88 // eslint-disable-next-line @next/next/no-img-element
89 <img src={user
90 ) : (
91 <DitherAvatar name={
92 )}
93 </div>
94 <span className="truncate text-sm text-foreground
95 </div>
96 );
97}
98
99function getEventDetails(eventType:
100 if (!propertiesJson)
101 let props:
102 try { props =
103
104 if (eventType ===
105 const sign =
106 return `${sign}${
107 }
108 if (eventType ===
109 return `${props.
110 }
111 if (typeof props.
112 if (typeof props.
113 return props.long
114 }
115 if (typeof props.
116 if (typeof props.
117 if (typeof props.
118 if (typeof props.
119 return props.targetUserId
120 }
121 if (typeof props.
122 if (typeof props.
123 if (eventType.startsWith
124 return [props.type
125 }
126 if (typeof props.
127 return null;
128}
129
130function fmtDate(d:
131 if (!d)
132 return new Date
133}
134
135export default function AdminActivityPage
136 const t =
137 const eventLabel =
138 const router =
139 const { data:
140
141 useEffect(() =>
142 if (!isPending &&
143 if (!isPending &&
144 }, [isPending,
145
146 const { data:
147 ...orpc.activity.
148 enabled: !!session &&
149 });
150
151 if (isPending ||
152
153 const entries =
154 const serverCount =
155 const userCount =
156 const infraCount =
157 e.eventType.startsWith
158 e.eventType.startsWith
159 e.eventType.startsWith
160 e.eventType.startsWith
161 e.eventType.startsWith
162 e.eventType.startsWith
163 e.eventType.startsWith
164 e.eventType.startsWith
165 ).length;
166 const billingCount =
167
168 return (
169 <>
170 <div className="flex flex-1 flex-col gap-3 overflow-auto px-4 py-4 md:flex-row md:overflow-hidden
171 <div className="flex flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card
172 <div className="grid grid-cols-[160px_180px_200px_150px_1fr_130px] border-b border-border bg-muted/40 px-4 py-2.5
173 <span className="text-xs font-medium text-muted-foreground
174 <span className="text-xs font-medium text-muted-foreground
175 <span className="text-xs font-medium text-muted-foreground
176 <span className="text-xs font-medium text-muted-foreground
177 <span className="text-xs font-medium text-muted-foreground
178 <span className="text-xs font-medium text-muted-foreground
179 </div>
180
181 <div className="flex-1 overflow-y-auto
182 {activityPending ? (
183 <div className="flex items-center justify-center py-12 text-sm text-muted-foreground
184 ) : entries.length
185 <div className="flex flex-col items-center justify-center py-12 gap-2
186 <Activity className="
187 <p className="text-sm text-muted-foreground
188 </div>
189 ) : (
190 entries.map((entry
191 const style =
192 const isLast =
193 const target =
194 return (
195 <div
196 key={entry.id
197 className={`grid grid-cols-[160px_180px_200px_150px_1fr_130px] items-center px-4 py-3 transition-colors hover:bg-muted/40 ${!
198 >
199 <span className="font-mono text-xs text-muted-foreground
200 <span>
201 <span
202 className="rounded-full px-2 py-0.5 font-mono text-xs font-medium"
203 style={{ color:
204 >
205 {eventLabel(entry.eventType
206 </span>
207 </span>
208 <div className="min-w-0 pr-4
209 <ActorCell user={
210 </div>
211 <span className="truncate pr-4 font-mono text-xs text-muted-foreground
212 {target ?? "—
213 </span>
214 <span className="truncate pr-4 font-mono text-xs text-muted-foreground
215 {getEventDetails(entry.eventType
216 </span>
217 <span className="font-mono text-xs text-muted-foreground
218 </div>
219 );
220 })
221 )}
222 </div>
223 </div>
224
225 <aside className="flex w-full shrink-0 flex-col overflow-hidden rounded-xl border border-border bg-card md:w-[220px]
226 <StatRow icon={
227 <span className="text-xl font-bold text-foreground
228 </StatRow>
229 <StatRow icon={
230 <span className="text-xl font-bold
231 </StatRow>
232 <StatRow icon={
233 <span className="text-xl font-bold
234 </StatRow>
235 <StatRow icon={
236 <span className="text-xl font-bold
237 </StatRow>
238 <StatRow icon={
239 <span className="text-xl font-bold
240 </StatRow>
241 </aside>
242 </div>
243 </>
244 );
245}
"
react
"
;
from
"
next/navigation
"
;
from
"
@tanstack/react-query
"
;
from
"
next-intl
"
;
,
Users
,
Layers
,
CreditCard
}
from
"
lucide-react
"
;
}
from
"
lucide-react
"
;
from
"
@/utils/orpc
"
;
from
"
@/lib/auth-client
"
;
"
;
from
"
@struxa/ui/components/dither-kit/avatar
"
;
;
;
.
ReactNode
;
"
>
"
>
"
>
h-3 w-3
"
/>
<
string
,
{
color
:
string
;
bg
:
string
}
>
=
{
#3b82f6
"
,
bg
:
"
rgba(59,130,246,0.12)
"
}
,
#f43f5e
"
,
bg
:
"
rgba(244,63,94,0.12)
"
}
,
#f59e0b
"
,
bg
:
"
rgba(245,158,11,0.12)
"
}
,
#f59e0b
"
,
bg
:
"
rgba(245,158,11,0.12)
"
}
,
#a855f7
"
,
bg
:
"
rgba(168,85,247,0.12)
"
}
,
#a855f7
"
,
bg
:
"
rgba(168,85,247,0.12)
"
}
,
#3b82f6
"
,
bg
:
"
rgba(59,130,246,0.12)
"
}
,
#f59e0b
"
,
bg
:
"
rgba(245,158,11,0.12)
"
}
,
#71717a
"
,
bg
:
"
rgba(113,113,122,0.10)
"
}
,
#10b981
"
,
bg
:
"
rgba(16,185,129,0.12)
"
}
,
#14b8a6
"
,
bg
:
"
rgba(20,184,166,0.12)
"
}
,
string
)
:
{
color
:
string
;
bg
:
string
}
{
(
"
admin:server.
"
))
return
EVENT_STYLES
.
server
!
;
(
"
admin:user.
"
))
return
EVENT_STYLES
.
user
!
;
(
"
admin:node.
"
))
return
EVENT_STYLES
.
node
!
;
(
"
admin:allocation.
"
))
return
EVENT_STYLES
.
alloc
!
;
(
"
admin:nest.
"
))
return
EVENT_STYLES
.
nest
!
;
(
"
admin:egg.
"
))
return
EVENT_STYLES
.
egg
!
;
(
"
admin:database-host.
"
))
return
EVENT_STYLES
.
dbhost
!
;
(
"
admin:location.
"
))
return
EVENT_STYLES
.
location
!
;
(
"
admin:settings.
"
))
return
EVENT_STYLES
.
settings
!
;
(
"
admin:billing.
"
))
return
EVENT_STYLES
.
billing
!
;
(
"
admin:backup-destination.
"
))
return
EVENT_STYLES
.
backup
!
;
"
#71717a
"
,
bg
:
"
rgba(113,113,122,0.10)
"
}
;
slug
:
string
)
=>
.
_-
]
/
g
,
"
"
)
.
replace
(
/
\b
\w
/
g
,
(
c
)
=>
c
.
toUpperCase
())
;
useTranslations
(
"
admin.activity.events
"
)
;
string
)
=>
{
event
.
replace
(
/
^
admin:
/
,
""
)
;
path
.
lastIndexOf
(
"
.
"
)
;
0
)
return
prettify
(
path
)
;
(
kind
:
string
,
slug
:
string
)
=>
kind
}
.
${
slug
}
`
)
?
t
(
`
${
kind
}
.
${
slug
}
`
)
:
t
(
"
unknown
"
)
;
"
resource
"
,
path
.
slice
(
0
,
dot
)
.
replace
(
/
\.
/
g
,
"
_
"
))
}
:
${
part
(
"
action
"
,
path
.
slice
(
dot
+
1
))
}
`
;
}
:
{
user
:
{
name
:
string
|
null
;
email
:
string
;
image
:
string
|
null
}
|
null
|
undefined
})
{
useTranslations
(
"
admin.activity
"
)
;
return
<
span
className
=
"
text-sm text-muted-foreground
"
>
{
t
(
"
system
"
)
}
</
span
>
;
user
.
name
??
user
.
email
;
"
>
"
>
(
.
image
}
alt
=
{
displayName
}
className
=
"
h-6 w-6 object-cover
"
/>
displayName
}
className
=
"
h-6 w-6
"
/>
"
>
{
displayName
}
</
span
>
string
,
propertiesJson
:
string
|
null
)
:
string
|
null
{
return
null
;
Record
<
string
,
unknown
>
;
JSON
.
parse
(
propertiesJson
)
as
Record
<
string
,
unknown
>
;
}
catch
{
return
null
;
}
"
admin:billing.wallet.adjust
"
&&
typeof
props
.
amountCents
===
"
number
"
)
{
props
.
amountCents
>=
0
?
"
+
"
:
""
;
(
props
.
amountCents
/
100
)
.
toFixed
(
2
)
}
`
;
"
admin:billing.gateway.create
"
&&
typeof
props
.
name
===
"
string
"
&&
typeof
props
.
provider
===
"
string
"
)
{
name
}
(
${
props
.
provider
}
)
`
;
name
===
"
string
"
)
return
props
.
name
;
short
===
"
string
"
)
{
&&
typeof
props
.
long
===
"
string
"
?
`
${
props
.
short
}
—
${
props
.
long
}
`
:
props
.
short
;
variable
===
"
string
"
)
return
props
.
variable
;
targetUserId
===
"
string
"
)
{
role
===
"
string
"
)
return
`
→
${
props
.
role
}
`
;
reason
===
"
string
"
)
return
props
.
reason
;
;
ip
===
"
string
"
&&
typeof
props
.
ports
===
"
string
"
)
return
`
${
props
.
ip
}
:
${
props
.
ports
}
`
;
ip
===
"
string
"
&&
typeof
props
.
port
===
"
number
"
)
return
`
${
props
.
ip
}
:
${
props
.
port
}
`
;
(
"
admin:backup-destination.
"
))
{
,
props
.
scope
]
.
filter
(
(
v
)
=>
typeof
v
===
"
string
"
)
.
join
(
"
·
"
)
||
null
;
key
===
"
string
"
)
return
props
.
key
;
Date
|
string
|
null
)
:
string
{
return
"
—
"
;
(
d
)
.
toLocaleString
()
;
()
{
useTranslations
(
"
admin.activity
"
)
;
useEventLabel
()
;
useRouter
()
;
session
,
isPending
}
=
authClient
.
useSession
()
;
{
!
session
)
router
.
replace
(
"
/login
"
)
;
session
&&
session
.
user
.
role
!==
"
admin
"
)
router
.
replace
(
"
/
"
)
;
session
,
router
])
;
activityData
,
isPending
:
activityPending
}
=
useQuery
(
{
adminList
.
queryOptions
(
{
input
:
{
page
:
1
}
}
)
,
session
.
user
.
role
===
"
admin
"
,
!
session
)
return
<
Loader
/>
;
activityData
?.
data
??
[]
;
entries
.
filter
(
(
e
)
=>
e
.
eventType
.
startsWith
(
"
admin:server.
"
))
.
length
;
entries
.
filter
(
(
e
)
=>
e
.
eventType
.
startsWith
(
"
admin:user.
"
))
.
length
;
entries
.
filter
(
(
e
)
=>
(
"
admin:node.
"
)
||
(
"
admin:allocation.
"
)
||
(
"
admin:location.
"
)
||
(
"
admin:database-host.
"
)
||
(
"
admin:nest.
"
)
||
(
"
admin:egg.
"
)
||
(
"
admin:settings.
"
)
||
(
"
admin:backup-destination.
"
)
entries
.
filter
(
(
e
)
=>
e
.
eventType
.
startsWith
(
"
admin:billing.
"
))
.
length
;
"
>
"
>
"
>
"
>
{
t
(
"
timestampColumn
"
)
}
</
span
>
"
>
{
t
(
"
eventColumn
"
)
}
</
span
>
"
>
{
t
(
"
actorColumn
"
)
}
</
span
>
"
>
{
t
(
"
targetColumn
"
)
}
</
span
>
"
>
{
t
(
"
detailsColumn
"
)
}
</
span
>
"
>
{
t
(
"
ipColumn
"
)
}
</
span
>
"
>
"
>
{
t
(
"
loading
"
)
}
</
div
>
===
0
?
(
"
>
h-8 w-8 text-muted-foreground/30
"
/>
"
>
{
t
(
"
noActivity
"
)
}
</
p
>
,
i
)
=>
{
getEventStyle
(
entry
.
eventType
)
;
i
===
entries
.
length
-
1
;
entry
.
server
?.
name
??
entry
.
node
?.
name
??
null
;
}
isLast
?
"
border-b border-border
"
:
""
}
`
}
"
>
{
fmtDate
(
entry
.
timestamp
)
}
</
span
>
style
.
color
,
backgroundColor
:
style
.
bg
}
}
)
}
"
>
entry
.
user
}
/>
"
>
"
}
"
>
,
entry
.
properties
)
??
"
—
"
}
"
>
{
entry
.
ip
??
"
—
"
}
</
span
>
"
>
Activity
}
label
=
{
t
(
"
totalEvents
"
)
}
>
"
>
{
entries
.
length
}
</
span
>
Server
}
label
=
{
t
(
"
serverEvents
"
)
}
>
"
style
=
{
{
color
:
"
#3b82f6
"
}
}
>
{
serverCount
}
</
span
>
Users
}
label
=
{
t
(
"
userModeration
"
)
}
>
"
style
=
{
{
color
:
"
#f43f5e
"
}
}
>
{
userCount
}
</
span
>
Layers
}
label
=
{
t
(
"
infrastructure
"
)
}
>
"
style
=
{
{
color
:
"
#f59e0b
"
}
}
>
{
infraCount
}
</
span
>
CreditCard
}
label
=
{
t
(
"
billingEvents
"
)
}
>
"
style
=
{
{
color
:
"
#10b981
"
}
}
>
{
billingCount
}
</
span
>