Ghost

struxadotcloud/struxa

public
Fork0
CodeIssues0Pull requests0
1branch
struxaappswebsrcapp(panel)servers[id]activitypage.tsx
feat: global unslop6531421a month ago220 lines · 10.5 KB
1"use client";
2
3import { useEffect } from
4import { use }
5import { useRouter }
6import { useQuery }
7import { Activity, Power
8import type { LucideIcon
9import { useTranslations }
10import { orpc }
11import { authClient }
12import Loader from "@/components/loader
13import { DitherAvatar }
14
15function StatRow({
16 icon: Icon,
17 label,
18 children,
19}: {
20 icon: LucideIcon
21 label: string
22 children: React
23}) {
24 return (
25 <div className="flex flex-col border-b border-border last:border-b-0
26 <div className="flex flex-col gap-1.5 px-4 pt-3 pb-2.5
27 <div className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground
28 <Icon className="
29 {label}
30 </div>
31 {children}
32 </div>
33 </div>
34 );
35}
36
37const EVENT_STYLES: Record
38 "server:power.start": { color:
39 "server:power.restart": { color:
40 "server:power.stop": { color:
41 backup: { color: "
42 files: { color: "
43 console: { color: "
44 user: { color: "
45};
46
47function getEventStyle(event:
48 if (event.startsWith
49 if (event.startsWith
50 if (event.startsWith
51 if (event.startsWith
52 if (event.startsWith
53 if (event ===
54 if (event.startsWith
55 if (event.startsWith
56 return { color:
57}
58
59const EVENT_LABEL_KEYS: Record
60 "server:power.start": "eventPowerStart",
61 "server:power.stop": "eventPowerStop",
62 "server:power.restart": "eventPowerRestart",
63 "server:power.kill": "eventPowerKill",
64 "server:backup.start": "eventBackupCreate",
65 "server:backup.delete": "eventBackupDelete",
66 "server:backup.restore": "eventBackupRestore",
67 "server:files.read": "eventFilesRead",
68 "server:files.write": "eventFilesWrite",
69 "server:files.rename": "eventFilesRename",
70 "server:files.delete": "eventFilesDelete",
71 "server:settings": "eventSettingsUpdate",
72 "server:reinstall": "eventServerReinstall",
73 "server:database.create": "eventDatabaseCreate",
74 "server:database.delete": "eventDatabaseDelete",
75 "server:database.rotate-password": "eventDatabaseRotatePassword",
76 "server:schedule.create": "eventScheduleCreate",
77 "server:schedule.update": "eventScheduleUpdate",
78 "server:schedule.delete": "eventScheduleDelete",
79 "user:subuser-create": "eventUsersAdd",
80 "user:subuser-update": "eventUsersUpdate",
81 "user:subuser-delete": "eventUsersRemove",
82};
83
84function ActorCell({ user
85 if (!user)
86 const displayName =
87 return (
88 <div className="flex items-center gap-2 min-w-0
89 <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
90 {user.image ?
91 // eslint-disable-next-line @next/next/no-img-element
92 <img src={user
93 ) : (
94 <DitherAvatar name={
95 )}
96 </div>
97 <span className="truncate text-sm text-foreground
98 </div>
99 );
100}
101
102function getEventDetails(eventType:
103 if (!propertiesJson)
104 let props:
105 try { props =
106
107 if (eventType.startsWith
108 if (eventType.startsWith
109 if (eventType.startsWith
110 if (eventType.startsWith
111 if (eventType ===
112 if (typeof props.
113 if (typeof props.
114 if (typeof props.
115 }
116 if (eventType ===
117 return null;
118}
119
120function fmtDate(d:
121 if (!d)
122 return new Date
123}
124
125export default function ActivityPage
126 const t =
127 const router =
128 const { data:
129 const { id
130
131 useEffect(() =>
132 if (!isPending &&
133 }, [isPending,
134
135 const { data:
136 const serverId =
137
138 const { data:
139 ...orpc.activity.
140 enabled: !!serverId,
141 });
142
143 if (isPending ||
144
145 const entries =
146 const powerCount =
147 const fileCount =
148 const backupCount =
149
150 return (
151 <>
152 <div className="flex flex-1 flex-col gap-3 overflow-auto px-4 py-4 md:flex-row md:overflow-hidden
153 <div className="flex flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card
154 {/* Table header */}
155 <div className="grid grid-cols-[160px_180px_200px_1fr_130px] border-b border-border bg-muted/40 px-4 py-2.5
156 <span className="text-xs font-medium text-muted-foreground
157 <span className="text-xs font-medium text-muted-foreground
158 <span className="text-xs font-medium text-muted-foreground
159 <span className="text-xs font-medium text-muted-foreground
160 <span className="text-xs font-medium text-muted-foreground
161 </div>
162
163 <div className="flex-1 overflow-y-auto
164 {activityPending ? (
165 <div className="flex items-center justify-center py-12 text-sm text-muted-foreground
166 ) : entries.length
167 <div className="flex flex-col items-center justify-center py-12 gap-2
168 <Activity className="
169 <p className="text-sm text-muted-foreground
170 </div>
171 ) : (
172 entries.map((entry
173 const style =
174 const isLast =
175 return (
176 <div
177 key={entry.id
178 className={`grid grid-cols-[160px_180px_200px_1fr_130px] items-center px-4 py-3 transition-colors hover:bg-muted/40 ${!
179 >
180 <span className="font-mono text-xs text-muted-foreground
181 <span>
182 <span
183 className="rounded-full px-2 py-0.5 font-mono text-xs font-medium"
184 style={{ color:
185 >
186 {EVENT_LABEL_KEYS[entry.eventType
187 </span>
188 </span>
189 <div className="min-w-0 pr-4
190 <ActorCell user={
191 </div>
192 <span className="truncate pr-4 font-mono text-xs text-muted-foreground
193 {getEventDetails(entry.eventType
194 </span>
195 <span className="font-mono text-xs text-muted-foreground
196 </div>
197 );
198 })
199 )}
200 </div>
201 </div>
202
203 <aside className="flex w-full shrink-0 flex-col overflow-hidden rounded-xl border border-border bg-card md:w-[220px]
204 <StatRow icon={
205 <span className="text-xl font-bold text-foreground
206 </StatRow>
207 <StatRow icon={
208 <span className="text-xl font-bold text-foreground
209 </StatRow>
210 <StatRow icon={
211 <span className="text-xl font-bold
212 </StatRow>
213 <StatRow icon={
214 <span className="text-xl font-bold
215 </StatRow>
216 </aside>
217 </div>
218 </>
219 );
220}
"
react
"
;
from
"
react
"
;
from
"
next/navigation
"
;
from
"
@tanstack/react-query
"
;
,
FileText
,
HardDrive
}
from
"
lucide-react
"
;
}
from
"
lucide-react
"
;
from
"
next-intl
"
;
from
"
@/utils/orpc
"
;
from
"
@/lib/auth-client
"
;
"
;
from
"
@struxa/ui/components/dither-kit/avatar
"
;
;
;
.
ReactNode
;
"
>
"
>
"
>
h-3 w-3
"
/>
<
string
,
{
color
:
string
;
bg
:
string
}
>
=
{
"
#22c55e
"
,
bg
:
"
rgba(34,197,94,0.12)
"
}
,
"
#22c55e
"
,
bg
:
"
rgba(34,197,94,0.12)
"
}
,
"
#f43f5e
"
,
bg
:
"
rgba(244,63,94,0.12)
"
}
,
#a855f7
"
,
bg
:
"
rgba(168,85,247,0.12)
"
}
,
#3b82f6
"
,
bg
:
"
rgba(59,130,246,0.12)
"
}
,
#71717a
"
,
bg
:
"
rgba(113,113,122,0.10)
"
}
,
#71717a
"
,
bg
:
"
rgba(113,113,122,0.10)
"
}
,
string
)
{
(
"
server:power.start
"
)
||
event
===
"
server:power.restart
"
)
return
EVENT_STYLES
[
"
server:power.start
"
]
!
;
(
"
server:power.stop
"
))
return
EVENT_STYLES
[
"
server:power.stop
"
]
!
;
(
"
server:backup
"
))
return
EVENT_STYLES
.
backup
!
;
(
"
server:files
"
))
return
EVENT_STYLES
.
files
!
;
(
"
server:console
"
))
return
EVENT_STYLES
.
console
!
;
"
server:reinstall
"
)
return
{
color
:
"
#3b82f6
"
,
bg
:
"
rgba(59,130,246,0.12)
"
}
;
(
"
server:database
"
))
return
{
color
:
"
#f59e0b
"
,
bg
:
"
rgba(245,158,11,0.12)
"
}
;
(
"
user:
"
))
return
EVENT_STYLES
.
user
!
;
"
#71717a
"
,
bg
:
"
rgba(113,113,122,0.10)
"
}
;
<
string
,
string
>
=
{
,
systemLabel
}
:
{
user
:
{
name
:
string
|
null
;
email
:
string
;
image
:
string
|
null
}
|
null
|
undefined
;
systemLabel
:
string
})
{
return
<
span
className
=
"
text-sm text-muted-foreground
"
>
{
systemLabel
}
</
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
;
}
(
"
server:files
"
)
&&
typeof
props
.
file
===
"
string
"
)
return
props
.
file
;
(
"
server:backup
"
)
&&
typeof
props
.
name
===
"
string
"
)
return
props
.
name
;
(
"
server:database
"
)
&&
typeof
props
.
database
===
"
string
"
)
return
props
.
database
;
(
"
server:schedule
"
)
&&
typeof
props
.
name
===
"
string
"
)
return
props
.
name
;
"
server:settings
"
)
{
name
===
"
string
"
)
return
`
Renamed →
${
props
.
name
}
`
;
image
===
"
string
"
)
return
`
Image →
${
props
.
image
}
`
;
envVariable
===
"
string
"
)
return
`
Variable:
${
props
.
envVariable
}
`
;
"
user:subuser-create
"
&&
typeof
props
.
targetEmail
===
"
string
"
)
return
props
.
targetEmail
;
Date
|
string
|
null
)
:
string
{
return
"
-
"
;
(
d
)
.
toLocaleString
()
;
(
{
params
}
:
{
params
:
Promise
<
{
id
:
string
}
>
})
{
useTranslations
(
"
panel.activity
"
)
;
useRouter
()
;
session
,
isPending
}
=
authClient
.
useSession
()
;
}
=
use
(
params
)
;
{
!
session
)
router
.
replace
(
"
/login
"
)
;
session
,
router
])
;
server
}
=
useQuery
(
orpc
.
servers
.
get
.
queryOptions
(
{
input
:
{
id
}
}
))
;
server
?.
id
;
activityData
,
isPending
:
activityPending
}
=
useQuery
(
{
list
.
queryOptions
(
{
input
:
{
serverId
:
serverId
??
""
,
page
:
1
}
}
)
,
!
session
)
return
<
Loader
/>
;
activityData
?.
data
??
[]
;
entries
.
filter
(
(
e
)
=>
e
.
eventType
.
startsWith
(
"
server:power
"
))
.
length
;
entries
.
filter
(
(
e
)
=>
e
.
eventType
.
startsWith
(
"
server:files
"
))
.
length
;
entries
.
filter
(
(
e
)
=>
e
.
eventType
.
startsWith
(
"
server:backup
"
))
.
length
;
"
>
"
>
"
>
"
>
{
t
(
"
colTimestamp
"
)
}
</
span
>
"
>
{
t
(
"
colEvent
"
)
}
</
span
>
"
>
{
t
(
"
colActor
"
)
}
</
span
>
"
>
{
t
(
"
colDetails
"
)
}
</
span
>
"
>
{
t
(
"
colIp
"
)
}
</
span
>
"
>
"
>
{
t
(
"
loading
"
)
}
</
div
>
===
0
?
(
"
>
h-8 w-8 text-muted-foreground/30
"
/>
"
>
{
t
(
"
empty
"
)
}
</
p
>
,
i
)
=>
{
getEventStyle
(
entry
.
eventType
)
;
i
===
entries
.
length
-
1
;
}
isLast
?
"
border-b border-border
"
:
""
}
`
}
"
>
{
fmtDate
(
entry
.
timestamp
)
}
</
span
>
style
.
color
,
backgroundColor
:
style
.
bg
}
}
]
?
t
(
EVENT_LABEL_KEYS
[
entry
.
eventType
])
:
entry
.
eventType
}
"
>
entry
.
user
}
systemLabel
=
{
t
(
"
system
"
)
}
/>
"
>
,
entry
.
properties
)
??
"
-
"
}
"
>
{
entry
.
ip
??
"
-
"
}
</
span
>
"
>
Activity
}
label
=
{
t
(
"
statEvents
"
)
}
>
"
>
{
entries
.
length
}
</
span
>
Power
}
label
=
{
t
(
"
statPowerEvents
"
)
}
>
"
>
{
powerCount
}
</
span
>
FileText
}
label
=
{
t
(
"
statFileWrites
"
)
}
>
"
style
=
{
{
color
:
"
#3b82f6
"
}
}
>
{
fileCount
}
</
span
>
HardDrive
}
label
=
{
t
(
"
statBackups
"
)
}
>
"
style
=
{
{
color
:
"
#a855f7
"
}
}
>
{
backupCount
}
</
span
>