launchbase 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/launchbase.js +2 -119
- package/package.json +1 -1
- package/template/frontend/src/App.tsx +6 -0
- package/template/frontend/src/components/Layout.tsx +6 -0
- package/template/frontend/src/lib/api.ts +0 -2
- package/template/frontend/src/lib/sdk.ts +1 -3
- package/template/frontend/src/pages/Deployments.tsx +332 -0
- package/template/frontend/src/pages/EdgeFunctions.tsx +478 -0
- package/template/prisma/migrations/0_init/migration.sql +262 -75
- package/template/prisma/schema.prisma +211 -147
- package/template/sdk/README.md +1 -2
- package/template/sdk/index.ts +1 -3
- package/template/src/modules/audit/audit.interceptor.ts +1 -1
- package/template/src/modules/auth/auth.service.ts +12 -7
- package/template/src/modules/billing/billing.service.ts +1 -1
- package/template/src/modules/common/filters/all-exceptions.filter.ts +15 -5
- package/template/src/modules/common/project.guard.ts +52 -0
- package/template/src/modules/common/tenant.guard.ts +3 -3
- package/template/src/modules/orgs/orgs.service.ts +18 -15
- package/template/src/modules/projects/dto/create-project.dto.ts +1 -5
- package/template/types/src/index.ts +0 -2
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import {
|
|
4
|
+
Code, Plus, Play, Trash2, Zap,
|
|
5
|
+
Search, Clock, Activity, RefreshCw
|
|
6
|
+
} from 'lucide-react'
|
|
7
|
+
import { useToast } from '@/components/Toast'
|
|
8
|
+
|
|
9
|
+
interface EdgeFunction {
|
|
10
|
+
id: string
|
|
11
|
+
name: string
|
|
12
|
+
slug: string
|
|
13
|
+
runtime: string
|
|
14
|
+
sourceCode: string
|
|
15
|
+
status: string
|
|
16
|
+
deployedAt: string | null
|
|
17
|
+
deploymentUrl: string | null
|
|
18
|
+
createdAt: string
|
|
19
|
+
updatedAt: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface EdgeFunctionLog {
|
|
23
|
+
id: string
|
|
24
|
+
status: string
|
|
25
|
+
duration: number
|
|
26
|
+
input: Record<string, any> | null
|
|
27
|
+
output: Record<string, any> | null
|
|
28
|
+
error: string | null
|
|
29
|
+
triggeredBy: string | null
|
|
30
|
+
createdAt: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const RUNTIMES = [
|
|
34
|
+
{ value: 'nodejs18', label: 'Node.js 18' },
|
|
35
|
+
{ value: 'nodejs20', label: 'Node.js 20' },
|
|
36
|
+
{ value: 'python311', label: 'Python 3.11' },
|
|
37
|
+
{ value: 'go121', label: 'Go 1.21' },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
export function EdgeFunctions() {
|
|
41
|
+
const queryClient = useQueryClient()
|
|
42
|
+
const toast = useToast()
|
|
43
|
+
|
|
44
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
45
|
+
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
46
|
+
const [showEditModal, setShowEditModal] = useState(false)
|
|
47
|
+
const [showLogsModal, setShowLogsModal] = useState(false)
|
|
48
|
+
const [selectedFunction, setSelectedFunction] = useState<EdgeFunction | null>(null)
|
|
49
|
+
const [functionLogs, setFunctionLogs] = useState<EdgeFunctionLog[]>([])
|
|
50
|
+
|
|
51
|
+
const [newFunction, setNewFunction] = useState({
|
|
52
|
+
name: '',
|
|
53
|
+
slug: '',
|
|
54
|
+
runtime: 'nodejs18',
|
|
55
|
+
sourceCode: `export default async function handler(req, res) {
|
|
56
|
+
// Your code here
|
|
57
|
+
res.json({ message: 'Hello World' })
|
|
58
|
+
}`,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Fetch functions
|
|
62
|
+
const { data: functionsData, isLoading } = useQuery({
|
|
63
|
+
queryKey: ['edge-functions'],
|
|
64
|
+
queryFn: async () => {
|
|
65
|
+
const res = await fetch('/api/functions')
|
|
66
|
+
if (!res.ok) throw new Error('Failed to fetch functions')
|
|
67
|
+
return res.json()
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const functions = functionsData?.functions || []
|
|
72
|
+
const stats = functionsData?.stats || { total: 0, deployed: 0, draft: 0, invocations: 0 }
|
|
73
|
+
|
|
74
|
+
// Create function mutation
|
|
75
|
+
const createMutation = useMutation({
|
|
76
|
+
mutationFn: async () => {
|
|
77
|
+
const res = await fetch('/api/functions', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify(newFunction)
|
|
81
|
+
})
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const error = await res.json()
|
|
84
|
+
throw new Error(error.message || 'Failed to create function')
|
|
85
|
+
}
|
|
86
|
+
return res.json()
|
|
87
|
+
},
|
|
88
|
+
onSuccess: () => {
|
|
89
|
+
queryClient.invalidateQueries({ queryKey: ['edge-functions'] })
|
|
90
|
+
setShowCreateModal(false)
|
|
91
|
+
setNewFunction({
|
|
92
|
+
name: '',
|
|
93
|
+
slug: '',
|
|
94
|
+
runtime: 'nodejs18',
|
|
95
|
+
sourceCode: `export default async function handler(req, res) {
|
|
96
|
+
// Your code here
|
|
97
|
+
res.json({ message: 'Hello World' })
|
|
98
|
+
}`,
|
|
99
|
+
})
|
|
100
|
+
toast.success('Function created successfully')
|
|
101
|
+
},
|
|
102
|
+
onError: (error: Error) => {
|
|
103
|
+
toast.error(error.message)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Deploy function mutation
|
|
108
|
+
const deployMutation = useMutation({
|
|
109
|
+
mutationFn: async (functionId: string) => {
|
|
110
|
+
const res = await fetch(`/api/functions/${functionId}/deploy`, {
|
|
111
|
+
method: 'POST'
|
|
112
|
+
})
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
const error = await res.json()
|
|
115
|
+
throw new Error(error.message || 'Failed to deploy function')
|
|
116
|
+
}
|
|
117
|
+
return res.json()
|
|
118
|
+
},
|
|
119
|
+
onSuccess: () => {
|
|
120
|
+
queryClient.invalidateQueries({ queryKey: ['edge-functions'] })
|
|
121
|
+
toast.success('Function deployed successfully')
|
|
122
|
+
},
|
|
123
|
+
onError: (error: Error) => {
|
|
124
|
+
toast.error(error.message)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Delete function mutation
|
|
129
|
+
const deleteMutation = useMutation({
|
|
130
|
+
mutationFn: async (functionId: string) => {
|
|
131
|
+
const res = await fetch(`/api/functions/${functionId}`, {
|
|
132
|
+
method: 'DELETE'
|
|
133
|
+
})
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
const error = await res.json()
|
|
136
|
+
throw new Error(error.message || 'Failed to delete function')
|
|
137
|
+
}
|
|
138
|
+
return res.json()
|
|
139
|
+
},
|
|
140
|
+
onSuccess: () => {
|
|
141
|
+
queryClient.invalidateQueries({ queryKey: ['edge-functions'] })
|
|
142
|
+
toast.success('Function deleted successfully')
|
|
143
|
+
},
|
|
144
|
+
onError: (error: Error) => {
|
|
145
|
+
toast.error(error.message)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Fetch logs
|
|
150
|
+
const fetchLogs = async (functionId: string) => {
|
|
151
|
+
try {
|
|
152
|
+
const res = await fetch(`/api/functions/${functionId}/logs`)
|
|
153
|
+
if (res.ok) {
|
|
154
|
+
const data = await res.json()
|
|
155
|
+
setFunctionLogs(data.logs || [])
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('Failed to fetch logs:', error)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const handleCreate = () => {
|
|
163
|
+
if (!newFunction.name.trim()) {
|
|
164
|
+
toast.error('Function name is required')
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
const slug = newFunction.slug || newFunction.name.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
|
168
|
+
createMutation.mutate()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const handleNameChange = (name: string) => {
|
|
172
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
|
173
|
+
setNewFunction({ ...newFunction, name, slug })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const filteredFunctions = functions.filter((f: EdgeFunction) =>
|
|
177
|
+
f.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
178
|
+
f.slug.toLowerCase().includes(searchQuery.toLowerCase())
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
const getStatusColor = (status: string) => {
|
|
182
|
+
switch (status) {
|
|
183
|
+
case 'deployed': return 'bg-green-100 text-green-700'
|
|
184
|
+
case 'draft': return 'bg-yellow-100 text-yellow-700'
|
|
185
|
+
case 'failed': return 'bg-red-100 text-red-700'
|
|
186
|
+
default: return 'bg-gray-100 text-gray-700'
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div>
|
|
192
|
+
<div className="flex items-center justify-between mb-6">
|
|
193
|
+
<div>
|
|
194
|
+
<h1 className="text-2xl font-bold text-gray-900">Edge Functions</h1>
|
|
195
|
+
<p className="text-gray-500 mt-1">Serverless functions for your project</p>
|
|
196
|
+
</div>
|
|
197
|
+
<button
|
|
198
|
+
onClick={() => setShowCreateModal(true)}
|
|
199
|
+
className="px-4 py-2 bg-primary text-white rounded-lg font-medium hover:bg-primary-600 flex items-center gap-2"
|
|
200
|
+
>
|
|
201
|
+
<Plus className="w-4 h-4" />
|
|
202
|
+
Create Function
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Stats */}
|
|
207
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
208
|
+
<div className="bg-white rounded-lg border p-4">
|
|
209
|
+
<div className="flex items-center gap-3">
|
|
210
|
+
<div className="p-2 bg-blue-100 rounded-lg">
|
|
211
|
+
<Code className="w-5 h-5 text-blue-600" />
|
|
212
|
+
</div>
|
|
213
|
+
<div>
|
|
214
|
+
<p className="text-2xl font-bold">{stats.total}</p>
|
|
215
|
+
<p className="text-sm text-gray-500">Functions</p>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="bg-white rounded-lg border p-4">
|
|
220
|
+
<div className="flex items-center gap-3">
|
|
221
|
+
<div className="p-2 bg-green-100 rounded-lg">
|
|
222
|
+
<Play className="w-5 h-5 text-green-600" />
|
|
223
|
+
</div>
|
|
224
|
+
<div>
|
|
225
|
+
<p className="text-2xl font-bold">{stats.deployed}</p>
|
|
226
|
+
<p className="text-sm text-gray-500">Deployed</p>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="bg-white rounded-lg border p-4">
|
|
231
|
+
<div className="flex items-center gap-3">
|
|
232
|
+
<div className="p-2 bg-yellow-100 rounded-lg">
|
|
233
|
+
<Clock className="w-5 h-5 text-yellow-600" />
|
|
234
|
+
</div>
|
|
235
|
+
<div>
|
|
236
|
+
<p className="text-2xl font-bold">{stats.draft}</p>
|
|
237
|
+
<p className="text-sm text-gray-500">Draft</p>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="bg-white rounded-lg border p-4">
|
|
242
|
+
<div className="flex items-center gap-3">
|
|
243
|
+
<div className="p-2 bg-purple-100 rounded-lg">
|
|
244
|
+
<Activity className="w-5 h-5 text-purple-600" />
|
|
245
|
+
</div>
|
|
246
|
+
<div>
|
|
247
|
+
<p className="text-2xl font-bold">{stats.invocations.toLocaleString()}</p>
|
|
248
|
+
<p className="text-sm text-gray-500">Invocations</p>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{/* Search */}
|
|
255
|
+
<div className="mb-6">
|
|
256
|
+
<div className="relative max-w-md">
|
|
257
|
+
<Search className="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
|
258
|
+
<input
|
|
259
|
+
type="text"
|
|
260
|
+
placeholder="Search functions..."
|
|
261
|
+
value={searchQuery}
|
|
262
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
263
|
+
className="w-full pl-10 pr-4 py-2 border rounded-lg"
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* Functions List */}
|
|
269
|
+
<div className="bg-white rounded-lg border overflow-hidden">
|
|
270
|
+
{isLoading ? (
|
|
271
|
+
<div className="p-8 text-center text-gray-500">Loading functions...</div>
|
|
272
|
+
) : filteredFunctions.length === 0 ? (
|
|
273
|
+
<div className="p-8 text-center">
|
|
274
|
+
<Zap className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
|
275
|
+
<p className="text-gray-500">No functions created yet</p>
|
|
276
|
+
<button
|
|
277
|
+
onClick={() => setShowCreateModal(true)}
|
|
278
|
+
className="mt-4 text-primary hover:underline"
|
|
279
|
+
>
|
|
280
|
+
Create your first function
|
|
281
|
+
</button>
|
|
282
|
+
</div>
|
|
283
|
+
) : (
|
|
284
|
+
<table className="w-full">
|
|
285
|
+
<thead className="bg-gray-50 border-b">
|
|
286
|
+
<tr>
|
|
287
|
+
<th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Name</th>
|
|
288
|
+
<th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Status</th>
|
|
289
|
+
<th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Runtime</th>
|
|
290
|
+
<th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Last Deployed</th>
|
|
291
|
+
<th className="text-right px-4 py-3 text-sm font-medium text-gray-500">Actions</th>
|
|
292
|
+
</tr>
|
|
293
|
+
</thead>
|
|
294
|
+
<tbody className="divide-y">
|
|
295
|
+
{filteredFunctions.map((func: EdgeFunction) => (
|
|
296
|
+
<tr key={func.id} className="hover:bg-gray-50">
|
|
297
|
+
<td className="px-4 py-3">
|
|
298
|
+
<div className="flex items-center gap-3">
|
|
299
|
+
<div className="w-8 h-8 bg-primary-100 rounded flex items-center justify-center">
|
|
300
|
+
<Code className="w-4 h-4 text-primary-600" />
|
|
301
|
+
</div>
|
|
302
|
+
<div>
|
|
303
|
+
<p className="font-medium text-gray-900">{func.name}</p>
|
|
304
|
+
<p className="text-sm text-gray-500">/{func.slug}</p>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</td>
|
|
308
|
+
<td className="px-4 py-3">
|
|
309
|
+
<span className={`px-2 py-1 text-xs rounded-full ${getStatusColor(func.status)}`}>
|
|
310
|
+
{func.status}
|
|
311
|
+
</span>
|
|
312
|
+
</td>
|
|
313
|
+
<td className="px-4 py-3 text-sm text-gray-500">{func.runtime}</td>
|
|
314
|
+
<td className="px-4 py-3 text-sm text-gray-500">
|
|
315
|
+
{func.deployedAt ? new Date(func.deployedAt).toLocaleDateString() : 'Never'}
|
|
316
|
+
</td>
|
|
317
|
+
<td className="px-4 py-3">
|
|
318
|
+
<div className="flex items-center justify-end gap-2">
|
|
319
|
+
<button
|
|
320
|
+
onClick={() => deployMutation.mutate(func.id)}
|
|
321
|
+
className="p-1 hover:bg-green-100 rounded"
|
|
322
|
+
title="Deploy"
|
|
323
|
+
disabled={deployMutation.isPending}
|
|
324
|
+
>
|
|
325
|
+
<Play className="w-4 h-4 text-gray-400 hover:text-green-500" />
|
|
326
|
+
</button>
|
|
327
|
+
<button
|
|
328
|
+
onClick={() => {
|
|
329
|
+
setSelectedFunction(func)
|
|
330
|
+
fetchLogs(func.id)
|
|
331
|
+
setShowLogsModal(true)
|
|
332
|
+
}}
|
|
333
|
+
className="p-1 hover:bg-blue-100 rounded"
|
|
334
|
+
title="View Logs"
|
|
335
|
+
>
|
|
336
|
+
<Activity className="w-4 h-4 text-gray-400 hover:text-blue-500" />
|
|
337
|
+
</button>
|
|
338
|
+
<button
|
|
339
|
+
onClick={() => {
|
|
340
|
+
if (confirm('Are you sure you want to delete this function?')) {
|
|
341
|
+
deleteMutation.mutate(func.id)
|
|
342
|
+
}
|
|
343
|
+
}}
|
|
344
|
+
className="p-1 hover:bg-red-100 rounded"
|
|
345
|
+
title="Delete"
|
|
346
|
+
>
|
|
347
|
+
<Trash2 className="w-4 h-4 text-gray-400 hover:text-red-500" />
|
|
348
|
+
</button>
|
|
349
|
+
</div>
|
|
350
|
+
</td>
|
|
351
|
+
</tr>
|
|
352
|
+
))}
|
|
353
|
+
</tbody>
|
|
354
|
+
</table>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Create Function Modal */}
|
|
359
|
+
{showCreateModal && (
|
|
360
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
361
|
+
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
362
|
+
<h2 className="text-xl font-bold mb-4">Create Edge Function</h2>
|
|
363
|
+
<div className="space-y-4">
|
|
364
|
+
<div>
|
|
365
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
366
|
+
<input
|
|
367
|
+
type="text"
|
|
368
|
+
value={newFunction.name}
|
|
369
|
+
onChange={(e) => handleNameChange(e.target.value)}
|
|
370
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
371
|
+
placeholder="my-function"
|
|
372
|
+
/>
|
|
373
|
+
</div>
|
|
374
|
+
<div>
|
|
375
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">Slug</label>
|
|
376
|
+
<input
|
|
377
|
+
type="text"
|
|
378
|
+
value={newFunction.slug}
|
|
379
|
+
onChange={(e) => setNewFunction({ ...newFunction, slug: e.target.value })}
|
|
380
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
381
|
+
placeholder="my-function"
|
|
382
|
+
/>
|
|
383
|
+
</div>
|
|
384
|
+
<div>
|
|
385
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">Runtime</label>
|
|
386
|
+
<select
|
|
387
|
+
value={newFunction.runtime}
|
|
388
|
+
onChange={(e) => setNewFunction({ ...newFunction, runtime: e.target.value })}
|
|
389
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
390
|
+
>
|
|
391
|
+
{RUNTIMES.map(r => (
|
|
392
|
+
<option key={r.value} value={r.value}>{r.label}</option>
|
|
393
|
+
))}
|
|
394
|
+
</select>
|
|
395
|
+
</div>
|
|
396
|
+
<div>
|
|
397
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">Code</label>
|
|
398
|
+
<textarea
|
|
399
|
+
value={newFunction.sourceCode}
|
|
400
|
+
onChange={(e) => setNewFunction({ ...newFunction, sourceCode: e.target.value })}
|
|
401
|
+
className="w-full px-3 py-2 border rounded-lg font-mono text-sm"
|
|
402
|
+
rows={12}
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
<div className="flex justify-end gap-3 mt-6">
|
|
407
|
+
<button
|
|
408
|
+
onClick={() => setShowCreateModal(false)}
|
|
409
|
+
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
|
|
410
|
+
>
|
|
411
|
+
Cancel
|
|
412
|
+
</button>
|
|
413
|
+
<button
|
|
414
|
+
onClick={handleCreate}
|
|
415
|
+
disabled={createMutation.isPending}
|
|
416
|
+
className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-600 disabled:opacity-50"
|
|
417
|
+
>
|
|
418
|
+
{createMutation.isPending ? 'Creating...' : 'Create Function'}
|
|
419
|
+
</button>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
)}
|
|
424
|
+
|
|
425
|
+
{/* Logs Modal */}
|
|
426
|
+
{showLogsModal && selectedFunction && (
|
|
427
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
428
|
+
<div className="bg-white rounded-lg p-6 w-full max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
429
|
+
<div className="flex items-center justify-between mb-4">
|
|
430
|
+
<h2 className="text-xl font-bold">Logs - {selectedFunction.name}</h2>
|
|
431
|
+
<button
|
|
432
|
+
onClick={() => fetchLogs(selectedFunction.id)}
|
|
433
|
+
className="p-2 hover:bg-gray-100 rounded"
|
|
434
|
+
>
|
|
435
|
+
<RefreshCw className="w-4 h-4" />
|
|
436
|
+
</button>
|
|
437
|
+
</div>
|
|
438
|
+
{functionLogs.length === 0 ? (
|
|
439
|
+
<p className="text-gray-500 text-center py-8">No logs yet</p>
|
|
440
|
+
) : (
|
|
441
|
+
<div className="space-y-2">
|
|
442
|
+
{functionLogs.map((log) => (
|
|
443
|
+
<div key={log.id} className="p-3 bg-gray-50 rounded-lg">
|
|
444
|
+
<div className="flex items-center justify-between mb-2">
|
|
445
|
+
<span className={`px-2 py-1 text-xs rounded ${
|
|
446
|
+
log.status === 'success' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
|
447
|
+
}`}>
|
|
448
|
+
{log.status}
|
|
449
|
+
</span>
|
|
450
|
+
<span className="text-xs text-gray-500">
|
|
451
|
+
{new Date(log.createdAt).toLocaleString()}
|
|
452
|
+
</span>
|
|
453
|
+
</div>
|
|
454
|
+
<p className="text-sm text-gray-600">Duration: {log.duration}ms</p>
|
|
455
|
+
{log.error && (
|
|
456
|
+
<p className="text-sm text-red-500 mt-1">{log.error}</p>
|
|
457
|
+
)}
|
|
458
|
+
</div>
|
|
459
|
+
))}
|
|
460
|
+
</div>
|
|
461
|
+
)}
|
|
462
|
+
<div className="flex justify-end mt-6">
|
|
463
|
+
<button
|
|
464
|
+
onClick={() => {
|
|
465
|
+
setShowLogsModal(false)
|
|
466
|
+
setFunctionLogs([])
|
|
467
|
+
}}
|
|
468
|
+
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
|
|
469
|
+
>
|
|
470
|
+
Close
|
|
471
|
+
</button>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
)
|
|
478
|
+
}
|