@nimbleflux/fluxbase-sdk-react 2026.3.6-rc.1
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/.nvmrc +1 -0
- package/README-ADMIN.md +1076 -0
- package/README.md +195 -0
- package/examples/AdminDashboard.tsx +513 -0
- package/examples/README.md +163 -0
- package/package.json +66 -0
- package/src/context.test.tsx +147 -0
- package/src/context.tsx +33 -0
- package/src/index.test.ts +255 -0
- package/src/index.ts +175 -0
- package/src/test-setup.ts +22 -0
- package/src/test-utils.tsx +215 -0
- package/src/use-admin-auth.test.ts +175 -0
- package/src/use-admin-auth.ts +187 -0
- package/src/use-admin-hooks.test.ts +457 -0
- package/src/use-admin-hooks.ts +309 -0
- package/src/use-auth-config.test.ts +145 -0
- package/src/use-auth-config.ts +101 -0
- package/src/use-auth.test.ts +313 -0
- package/src/use-auth.ts +164 -0
- package/src/use-captcha.test.ts +273 -0
- package/src/use-captcha.ts +250 -0
- package/src/use-client-keys.test.ts +286 -0
- package/src/use-client-keys.ts +185 -0
- package/src/use-graphql.test.ts +424 -0
- package/src/use-graphql.ts +392 -0
- package/src/use-query.test.ts +348 -0
- package/src/use-query.ts +211 -0
- package/src/use-realtime.test.ts +359 -0
- package/src/use-realtime.ts +180 -0
- package/src/use-saml.test.ts +269 -0
- package/src/use-saml.ts +221 -0
- package/src/use-storage.test.ts +549 -0
- package/src/use-storage.ts +508 -0
- package/src/use-table-export.ts +481 -0
- package/src/use-users.test.ts +264 -0
- package/src/use-users.ts +198 -0
- package/tsconfig.json +28 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +11 -0
- package/typedoc.json +33 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete Admin Dashboard Example
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates all admin hooks in a real-world dashboard interface.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Admin authentication with protected routes
|
|
8
|
+
* - User management with pagination
|
|
9
|
+
* - API key management
|
|
10
|
+
* - Webhook configuration
|
|
11
|
+
* - App and system settings
|
|
12
|
+
* - Real-time statistics
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { useState } from 'react'
|
|
16
|
+
import {
|
|
17
|
+
useAdminAuth,
|
|
18
|
+
useUsers,
|
|
19
|
+
useAPIKeys,
|
|
20
|
+
useWebhooks,
|
|
21
|
+
useAppSettings,
|
|
22
|
+
useSystemSettings
|
|
23
|
+
} from '@nimbleflux/fluxbase-sdk-react'
|
|
24
|
+
import type { EnrichedUser } from '@nimbleflux/fluxbase-sdk'
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Admin Login Component
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
function AdminLogin() {
|
|
31
|
+
const { isAuthenticated, isLoading, error, login } = useAdminAuth({ autoCheck: true })
|
|
32
|
+
const [email, setEmail] = useState('')
|
|
33
|
+
const [password, setPassword] = useState('')
|
|
34
|
+
const [loginError, setLoginError] = useState<string | null>(null)
|
|
35
|
+
|
|
36
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
37
|
+
e.preventDefault()
|
|
38
|
+
setLoginError(null)
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await login(email, password)
|
|
42
|
+
} catch (err) {
|
|
43
|
+
setLoginError((err as Error).message)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (isLoading) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
50
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isAuthenticated) {
|
|
56
|
+
return null // Will be redirected by parent
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
61
|
+
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
|
|
62
|
+
<div>
|
|
63
|
+
<h2 className="text-center text-3xl font-extrabold text-gray-900">
|
|
64
|
+
Admin Login
|
|
65
|
+
</h2>
|
|
66
|
+
</div>
|
|
67
|
+
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
68
|
+
<div className="rounded-md shadow-sm -space-y-px">
|
|
69
|
+
<div>
|
|
70
|
+
<input
|
|
71
|
+
type="email"
|
|
72
|
+
required
|
|
73
|
+
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
|
74
|
+
placeholder="Email address"
|
|
75
|
+
value={email}
|
|
76
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
<div>
|
|
80
|
+
<input
|
|
81
|
+
type="password"
|
|
82
|
+
required
|
|
83
|
+
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
|
84
|
+
placeholder="Password"
|
|
85
|
+
value={password}
|
|
86
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{(loginError || error) && (
|
|
92
|
+
<div className="text-red-600 text-sm">
|
|
93
|
+
{loginError || error?.message}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
<div>
|
|
98
|
+
<button
|
|
99
|
+
type="submit"
|
|
100
|
+
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
101
|
+
>
|
|
102
|
+
Sign in
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</form>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Statistics Overview
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
function Overview() {
|
|
116
|
+
const { users, total: totalUsers, isLoading: loadingUsers } = useUsers({
|
|
117
|
+
autoFetch: true,
|
|
118
|
+
limit: 5
|
|
119
|
+
})
|
|
120
|
+
const { keys, isLoading: loadingKeys } = useAPIKeys({ autoFetch: true })
|
|
121
|
+
const { webhooks, isLoading: loadingWebhooks } = useWebhooks({ autoFetch: true })
|
|
122
|
+
const { settings, isLoading: loadingSettings } = useAppSettings({ autoFetch: true })
|
|
123
|
+
|
|
124
|
+
const isLoading = loadingUsers || loadingKeys || loadingWebhooks || loadingSettings
|
|
125
|
+
|
|
126
|
+
if (isLoading) {
|
|
127
|
+
return <div className="text-center py-8">Loading overview...</div>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const enabledWebhooks = webhooks.filter(w => w.enabled).length
|
|
131
|
+
const activeKeys = keys.filter(k => !k.expires_at || new Date(k.expires_at) > new Date()).length
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="space-y-6">
|
|
135
|
+
<h2 className="text-2xl font-bold">Dashboard Overview</h2>
|
|
136
|
+
|
|
137
|
+
{/* Statistics Cards */}
|
|
138
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
139
|
+
<div className="bg-white rounded-lg shadow p-6">
|
|
140
|
+
<div className="text-sm font-medium text-gray-500">Total Users</div>
|
|
141
|
+
<div className="mt-2 text-3xl font-semibold text-gray-900">{totalUsers}</div>
|
|
142
|
+
<div className="mt-2 text-xs text-gray-500">Registered accounts</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="bg-white rounded-lg shadow p-6">
|
|
146
|
+
<div className="text-sm font-medium text-gray-500">client keys</div>
|
|
147
|
+
<div className="mt-2 text-3xl font-semibold text-gray-900">{activeKeys}</div>
|
|
148
|
+
<div className="mt-2 text-xs text-gray-500">{keys.length - activeKeys} expired</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div className="bg-white rounded-lg shadow p-6">
|
|
152
|
+
<div className="text-sm font-medium text-gray-500">Webhooks</div>
|
|
153
|
+
<div className="mt-2 text-3xl font-semibold text-gray-900">{enabledWebhooks}</div>
|
|
154
|
+
<div className="mt-2 text-xs text-gray-500">{webhooks.length - enabledWebhooks} disabled</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div className="bg-white rounded-lg shadow p-6">
|
|
158
|
+
<div className="text-sm font-medium text-gray-500">Features</div>
|
|
159
|
+
<div className="mt-2 space-y-1">
|
|
160
|
+
<div className="flex items-center text-sm">
|
|
161
|
+
<span className={`w-2 h-2 rounded-full mr-2 ${settings?.features?.enable_realtime ? 'bg-green-500' : 'bg-gray-300'}`}></span>
|
|
162
|
+
Realtime
|
|
163
|
+
</div>
|
|
164
|
+
<div className="flex items-center text-sm">
|
|
165
|
+
<span className={`w-2 h-2 rounded-full mr-2 ${settings?.features?.enable_storage ? 'bg-green-500' : 'bg-gray-300'}`}></span>
|
|
166
|
+
Storage
|
|
167
|
+
</div>
|
|
168
|
+
<div className="flex items-center text-sm">
|
|
169
|
+
<span className={`w-2 h-2 rounded-full mr-2 ${settings?.features?.enable_functions ? 'bg-green-500' : 'bg-gray-300'}`}></span>
|
|
170
|
+
Functions
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Recent Users */}
|
|
177
|
+
<div className="bg-white rounded-lg shadow">
|
|
178
|
+
<div className="p-6">
|
|
179
|
+
<h3 className="text-lg font-medium mb-4">Recent Users</h3>
|
|
180
|
+
<div className="space-y-3">
|
|
181
|
+
{users.slice(0, 5).map((user) => (
|
|
182
|
+
<div key={user.id} className="flex items-center justify-between">
|
|
183
|
+
<div>
|
|
184
|
+
<div className="font-medium">{user.email}</div>
|
|
185
|
+
<div className="text-sm text-gray-500">
|
|
186
|
+
{new Date(user.created_at).toLocaleDateString()}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="flex items-center space-x-2">
|
|
190
|
+
<span className={`px-2 py-1 text-xs rounded ${
|
|
191
|
+
user.role === 'admin' ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-800'
|
|
192
|
+
}`}>
|
|
193
|
+
{user.role}
|
|
194
|
+
</span>
|
|
195
|
+
<span className={`px-2 py-1 text-xs rounded ${
|
|
196
|
+
user.email_confirmed ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
|
197
|
+
}`}>
|
|
198
|
+
{user.email_confirmed ? 'Verified' : 'Pending'}
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// User Management
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
function UserManagement() {
|
|
215
|
+
const [page, setPage] = useState(0)
|
|
216
|
+
const [searchEmail, setSearchEmail] = useState('')
|
|
217
|
+
const [roleFilter, setRoleFilter] = useState<'admin' | 'user' | ''>('')
|
|
218
|
+
const limit = 20
|
|
219
|
+
|
|
220
|
+
const {
|
|
221
|
+
users,
|
|
222
|
+
total,
|
|
223
|
+
isLoading,
|
|
224
|
+
error,
|
|
225
|
+
refetch,
|
|
226
|
+
inviteUser,
|
|
227
|
+
updateUserRole,
|
|
228
|
+
deleteUser,
|
|
229
|
+
resetPassword
|
|
230
|
+
} = useUsers({
|
|
231
|
+
autoFetch: true,
|
|
232
|
+
limit,
|
|
233
|
+
offset: page * limit,
|
|
234
|
+
email: searchEmail || undefined,
|
|
235
|
+
role: roleFilter || undefined
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const handleInvite = async () => {
|
|
239
|
+
const email = prompt('Enter user email:')
|
|
240
|
+
if (!email) return
|
|
241
|
+
|
|
242
|
+
const isAdmin = confirm('Grant admin privileges?')
|
|
243
|
+
try {
|
|
244
|
+
await inviteUser(email, isAdmin ? 'admin' : 'user')
|
|
245
|
+
alert('User invited successfully!')
|
|
246
|
+
} catch (err) {
|
|
247
|
+
alert('Failed to invite user: ' + (err as Error).message)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const handleRoleToggle = async (userId: string, currentRole: string) => {
|
|
252
|
+
const newRole = currentRole === 'admin' ? 'user' : 'admin'
|
|
253
|
+
if (confirm(`Change role to ${newRole}?`)) {
|
|
254
|
+
await updateUserRole(userId, newRole)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const handleDelete = async (userId: string, email: string) => {
|
|
259
|
+
if (confirm(`Delete user ${email}? This cannot be undone.`)) {
|
|
260
|
+
await deleteUser(userId)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const handleResetPassword = async (userId: string) => {
|
|
265
|
+
try {
|
|
266
|
+
const newPassword = await resetPassword(userId)
|
|
267
|
+
alert(`New password: ${newPassword}\n\nMake sure to save this - it won't be shown again!`)
|
|
268
|
+
} catch (err) {
|
|
269
|
+
alert('Failed to reset password: ' + (err as Error).message)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (error) {
|
|
274
|
+
return <div className="text-red-600">Error: {error.message}</div>
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div className="space-y-6">
|
|
279
|
+
<div className="flex justify-between items-center">
|
|
280
|
+
<h2 className="text-2xl font-bold">User Management</h2>
|
|
281
|
+
<button
|
|
282
|
+
onClick={handleInvite}
|
|
283
|
+
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
284
|
+
>
|
|
285
|
+
Invite User
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
{/* Filters */}
|
|
290
|
+
<div className="bg-white rounded-lg shadow p-4">
|
|
291
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
292
|
+
<input
|
|
293
|
+
type="text"
|
|
294
|
+
placeholder="Search by email..."
|
|
295
|
+
value={searchEmail}
|
|
296
|
+
onChange={(e) => {
|
|
297
|
+
setSearchEmail(e.target.value)
|
|
298
|
+
setPage(0)
|
|
299
|
+
}}
|
|
300
|
+
className="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
301
|
+
/>
|
|
302
|
+
<select
|
|
303
|
+
value={roleFilter}
|
|
304
|
+
onChange={(e) => {
|
|
305
|
+
setRoleFilter(e.target.value as any)
|
|
306
|
+
setPage(0)
|
|
307
|
+
}}
|
|
308
|
+
className="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
309
|
+
>
|
|
310
|
+
<option value="">All Roles</option>
|
|
311
|
+
<option value="admin">Admin</option>
|
|
312
|
+
<option value="user">User</option>
|
|
313
|
+
</select>
|
|
314
|
+
<button
|
|
315
|
+
onClick={refetch}
|
|
316
|
+
className="px-4 py-2 border border-gray-300 rounded hover:bg-gray-50"
|
|
317
|
+
>
|
|
318
|
+
Refresh
|
|
319
|
+
</button>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
{/* Users Table */}
|
|
324
|
+
<div className="bg-white rounded-lg shadow overflow-hidden">
|
|
325
|
+
{isLoading ? (
|
|
326
|
+
<div className="text-center py-8">Loading users...</div>
|
|
327
|
+
) : (
|
|
328
|
+
<>
|
|
329
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
330
|
+
<thead className="bg-gray-50">
|
|
331
|
+
<tr>
|
|
332
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
333
|
+
User
|
|
334
|
+
</th>
|
|
335
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
336
|
+
Role
|
|
337
|
+
</th>
|
|
338
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
339
|
+
Status
|
|
340
|
+
</th>
|
|
341
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
342
|
+
Created
|
|
343
|
+
</th>
|
|
344
|
+
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
345
|
+
Actions
|
|
346
|
+
</th>
|
|
347
|
+
</tr>
|
|
348
|
+
</thead>
|
|
349
|
+
<tbody className="bg-white divide-y divide-gray-200">
|
|
350
|
+
{users.map((user) => (
|
|
351
|
+
<tr key={user.id}>
|
|
352
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
353
|
+
<div className="text-sm font-medium text-gray-900">{user.email}</div>
|
|
354
|
+
<div className="text-xs text-gray-500">{user.id.substring(0, 8)}</div>
|
|
355
|
+
</td>
|
|
356
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
357
|
+
<span className={`px-2 py-1 text-xs rounded ${
|
|
358
|
+
user.role === 'admin' ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-800'
|
|
359
|
+
}`}>
|
|
360
|
+
{user.role}
|
|
361
|
+
</span>
|
|
362
|
+
</td>
|
|
363
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
364
|
+
<span className={`px-2 py-1 text-xs rounded ${
|
|
365
|
+
user.email_confirmed ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
|
366
|
+
}`}>
|
|
367
|
+
{user.email_confirmed ? 'Verified' : 'Pending'}
|
|
368
|
+
</span>
|
|
369
|
+
</td>
|
|
370
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
371
|
+
{new Date(user.created_at).toLocaleDateString()}
|
|
372
|
+
</td>
|
|
373
|
+
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
|
374
|
+
<button
|
|
375
|
+
onClick={() => handleRoleToggle(user.id, user.role)}
|
|
376
|
+
className="text-blue-600 hover:text-blue-900"
|
|
377
|
+
>
|
|
378
|
+
Toggle Role
|
|
379
|
+
</button>
|
|
380
|
+
<button
|
|
381
|
+
onClick={() => handleResetPassword(user.id)}
|
|
382
|
+
className="text-yellow-600 hover:text-yellow-900"
|
|
383
|
+
>
|
|
384
|
+
Reset PW
|
|
385
|
+
</button>
|
|
386
|
+
<button
|
|
387
|
+
onClick={() => handleDelete(user.id, user.email)}
|
|
388
|
+
className="text-red-600 hover:text-red-900"
|
|
389
|
+
>
|
|
390
|
+
Delete
|
|
391
|
+
</button>
|
|
392
|
+
</td>
|
|
393
|
+
</tr>
|
|
394
|
+
))}
|
|
395
|
+
</tbody>
|
|
396
|
+
</table>
|
|
397
|
+
|
|
398
|
+
{/* Pagination */}
|
|
399
|
+
<div className="bg-gray-50 px-6 py-3 flex items-center justify-between">
|
|
400
|
+
<div className="text-sm text-gray-700">
|
|
401
|
+
Showing {page * limit + 1} to {Math.min((page + 1) * limit, total)} of {total} users
|
|
402
|
+
</div>
|
|
403
|
+
<div className="space-x-2">
|
|
404
|
+
<button
|
|
405
|
+
onClick={() => setPage(page - 1)}
|
|
406
|
+
disabled={page === 0}
|
|
407
|
+
className="px-3 py-1 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100"
|
|
408
|
+
>
|
|
409
|
+
Previous
|
|
410
|
+
</button>
|
|
411
|
+
<button
|
|
412
|
+
onClick={() => setPage(page + 1)}
|
|
413
|
+
disabled={(page + 1) * limit >= total}
|
|
414
|
+
className="px-3 py-1 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100"
|
|
415
|
+
>
|
|
416
|
+
Next
|
|
417
|
+
</button>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// Main Dashboard
|
|
429
|
+
// ============================================================================
|
|
430
|
+
|
|
431
|
+
type TabType = 'overview' | 'users' | 'keys' | 'webhooks' | 'settings'
|
|
432
|
+
|
|
433
|
+
export default function AdminDashboard() {
|
|
434
|
+
const { user, isAuthenticated, isLoading, logout } = useAdminAuth({ autoCheck: true })
|
|
435
|
+
const [activeTab, setActiveTab] = useState<TabType>('overview')
|
|
436
|
+
|
|
437
|
+
if (isLoading) {
|
|
438
|
+
return (
|
|
439
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
440
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
441
|
+
</div>
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!isAuthenticated) {
|
|
446
|
+
return <AdminLogin />
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const tabs: { id: TabType; label: string }[] = [
|
|
450
|
+
{ id: 'overview', label: 'Overview' },
|
|
451
|
+
{ id: 'users', label: 'Users' },
|
|
452
|
+
{ id: 'keys', label: 'client keys' },
|
|
453
|
+
{ id: 'webhooks', label: 'Webhooks' },
|
|
454
|
+
{ id: 'settings', label: 'Settings' }
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<div className="min-h-screen bg-gray-100">
|
|
459
|
+
{/* Header */}
|
|
460
|
+
<header className="bg-white shadow">
|
|
461
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
462
|
+
<div className="flex justify-between items-center">
|
|
463
|
+
<h1 className="text-2xl font-bold text-gray-900">Admin Dashboard</h1>
|
|
464
|
+
<div className="flex items-center space-x-4">
|
|
465
|
+
<div className="text-sm text-gray-700">
|
|
466
|
+
<span className="font-medium">{user?.email}</span>
|
|
467
|
+
<span className="ml-2 px-2 py-1 text-xs rounded bg-purple-100 text-purple-800">
|
|
468
|
+
{user?.role}
|
|
469
|
+
</span>
|
|
470
|
+
</div>
|
|
471
|
+
<button
|
|
472
|
+
onClick={logout}
|
|
473
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900"
|
|
474
|
+
>
|
|
475
|
+
Logout
|
|
476
|
+
</button>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
</header>
|
|
481
|
+
|
|
482
|
+
{/* Navigation Tabs */}
|
|
483
|
+
<nav className="bg-white shadow-sm">
|
|
484
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
485
|
+
<div className="flex space-x-8">
|
|
486
|
+
{tabs.map((tab) => (
|
|
487
|
+
<button
|
|
488
|
+
key={tab.id}
|
|
489
|
+
onClick={() => setActiveTab(tab.id)}
|
|
490
|
+
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
|
491
|
+
activeTab === tab.id
|
|
492
|
+
? 'border-blue-500 text-blue-600'
|
|
493
|
+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
494
|
+
}`}
|
|
495
|
+
>
|
|
496
|
+
{tab.label}
|
|
497
|
+
</button>
|
|
498
|
+
))}
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
</nav>
|
|
502
|
+
|
|
503
|
+
{/* Main Content */}
|
|
504
|
+
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
505
|
+
{activeTab === 'overview' && <Overview />}
|
|
506
|
+
{activeTab === 'users' && <UserManagement />}
|
|
507
|
+
{activeTab === 'keys' && <div className="text-center py-8">client keys management (implement using useAPIKeys)</div>}
|
|
508
|
+
{activeTab === 'webhooks' && <div className="text-center py-8">Webhooks management (implement using useWebhooks)</div>}
|
|
509
|
+
{activeTab === 'settings' && <div className="text-center py-8">Settings management (implement using useAppSettings and useSystemSettings)</div>}
|
|
510
|
+
</main>
|
|
511
|
+
</div>
|
|
512
|
+
)
|
|
513
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# React Admin Hooks Examples
|
|
2
|
+
|
|
3
|
+
This directory contains complete, production-ready examples demonstrating the Fluxbase React admin hooks.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
### AdminDashboard.tsx
|
|
8
|
+
|
|
9
|
+
A complete admin dashboard application demonstrating all admin hooks in action.
|
|
10
|
+
|
|
11
|
+
**Features:**
|
|
12
|
+
- Admin authentication with protected routes
|
|
13
|
+
- User management with pagination and search
|
|
14
|
+
- Real-time statistics dashboard
|
|
15
|
+
- Modern UI with Tailwind CSS
|
|
16
|
+
- Full TypeScript support
|
|
17
|
+
|
|
18
|
+
**Hooks demonstrated:**
|
|
19
|
+
- `useAdminAuth()` - Authentication state management
|
|
20
|
+
- `useUsers()` - User CRUD operations with pagination
|
|
21
|
+
- `useAPIKeys()` - API key management
|
|
22
|
+
- `useWebhooks()` - Webhook configuration
|
|
23
|
+
- `useAppSettings()` - Application settings
|
|
24
|
+
- `useSystemSettings()` - System settings
|
|
25
|
+
|
|
26
|
+
**Run the example:**
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Install dependencies
|
|
30
|
+
npm install @fluxbase/sdk @fluxbase/sdk-react @tanstack/react-query
|
|
31
|
+
|
|
32
|
+
# Copy AdminDashboard.tsx to your project
|
|
33
|
+
# Add Tailwind CSS to your project (optional, for styling)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Basic usage:**
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { createClient } from '@fluxbase/sdk'
|
|
40
|
+
import { FluxbaseProvider } from '@fluxbase/sdk-react'
|
|
41
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
42
|
+
import AdminDashboard from './AdminDashboard'
|
|
43
|
+
|
|
44
|
+
const client = createClient({ url: 'http://localhost:8080' })
|
|
45
|
+
const queryClient = new QueryClient()
|
|
46
|
+
|
|
47
|
+
function App() {
|
|
48
|
+
return (
|
|
49
|
+
<QueryClientProvider client={queryClient}>
|
|
50
|
+
<FluxbaseProvider client={client}>
|
|
51
|
+
<AdminDashboard />
|
|
52
|
+
</FluxbaseProvider>
|
|
53
|
+
</QueryClientProvider>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Key Patterns
|
|
59
|
+
|
|
60
|
+
### 1. Authentication Flow
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
const { isAuthenticated, isLoading, login, logout } = useAdminAuth({
|
|
64
|
+
autoCheck: true // Automatically check auth status on mount
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (isLoading) return <LoadingSpinner />
|
|
68
|
+
if (!isAuthenticated) return <LoginPage />
|
|
69
|
+
return <Dashboard />
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Data Fetching with Pagination
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
const [page, setPage] = useState(0)
|
|
76
|
+
const limit = 20
|
|
77
|
+
|
|
78
|
+
const { users, total, isLoading } = useUsers({
|
|
79
|
+
autoFetch: true,
|
|
80
|
+
limit,
|
|
81
|
+
offset: page * limit
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 3. Search and Filters
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const [searchEmail, setSearchEmail] = useState('')
|
|
89
|
+
const [roleFilter, setRoleFilter] = useState<'admin' | 'user' | ''>('')
|
|
90
|
+
|
|
91
|
+
const { users, refetch } = useUsers({
|
|
92
|
+
autoFetch: true,
|
|
93
|
+
email: searchEmail || undefined,
|
|
94
|
+
role: roleFilter || undefined
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Refetch when filters change
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
refetch()
|
|
100
|
+
}, [searchEmail, roleFilter, refetch])
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4. Optimistic Updates
|
|
104
|
+
|
|
105
|
+
All mutation functions automatically refetch data:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
const { users, inviteUser, deleteUser } = useUsers({ autoFetch: true })
|
|
109
|
+
|
|
110
|
+
// These automatically refetch the user list after success
|
|
111
|
+
await inviteUser('new@example.com', 'user')
|
|
112
|
+
await deleteUser(userId)
|
|
113
|
+
// users state is now updated
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 5. Error Handling
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
const { data, error, isLoading, refetch } = useUsers({ autoFetch: true })
|
|
120
|
+
|
|
121
|
+
if (isLoading) return <LoadingSpinner />
|
|
122
|
+
if (error) return <ErrorMessage error={error} onRetry={refetch} />
|
|
123
|
+
return <DataDisplay data={data} />
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Styling
|
|
127
|
+
|
|
128
|
+
The example uses Tailwind CSS utility classes, but you can easily adapt it to your preferred styling solution:
|
|
129
|
+
|
|
130
|
+
- **CSS Modules**: Replace `className` with module imports
|
|
131
|
+
- **Styled Components**: Replace elements with styled components
|
|
132
|
+
- **Material-UI**: Use MUI components instead of native HTML
|
|
133
|
+
- **Chakra UI**: Use Chakra components for rapid development
|
|
134
|
+
|
|
135
|
+
## TypeScript
|
|
136
|
+
|
|
137
|
+
All examples are fully typed. The hooks provide complete type safety:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import type { EnrichedUser, APIKey, Webhook } from '@fluxbase/sdk-react'
|
|
141
|
+
|
|
142
|
+
const { users }: { users: EnrichedUser[] } = useUsers({ autoFetch: true })
|
|
143
|
+
const { keys }: { keys: APIKey[] } = useAPIKeys({ autoFetch: true })
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Next Steps
|
|
147
|
+
|
|
148
|
+
1. **Customize the UI** - Adapt the styling to match your design system
|
|
149
|
+
2. **Add more features** - Implement client keys, webhooks, and settings tabs
|
|
150
|
+
3. **Add validation** - Add form validation for user inputs
|
|
151
|
+
4. **Add loading states** - Improve loading and error states
|
|
152
|
+
5. **Add notifications** - Show success/error toasts for mutations
|
|
153
|
+
6. **Add search** - Enhance search with debouncing and advanced filters
|
|
154
|
+
|
|
155
|
+
## Documentation
|
|
156
|
+
|
|
157
|
+
- [Complete Admin Hooks Guide](../README-ADMIN.md) - Full API reference and examples
|
|
158
|
+
- [React Hooks Documentation](../../docs/docs/sdks/react-hooks.md) - Core hooks documentation
|
|
159
|
+
- [Admin API Documentation](../../docs/docs/sdk/admin.md) - Admin operations reference
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|