@fluxbase/sdk-react 0.0.1-rc.2
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/CHANGELOG.md +67 -0
- package/README-ADMIN.md +1076 -0
- package/README.md +178 -0
- package/dist/index.d.mts +606 -0
- package/dist/index.d.ts +606 -0
- package/dist/index.js +992 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +926 -0
- package/dist/index.mjs.map +1 -0
- package/examples/AdminDashboard.tsx +513 -0
- package/examples/README.md +163 -0
- package/package.json +52 -0
- package/src/context.tsx +33 -0
- package/src/index.ts +113 -0
- package/src/use-admin-auth.ts +168 -0
- package/src/use-admin-hooks.ts +309 -0
- package/src/use-api-keys.ts +174 -0
- package/src/use-auth.ts +146 -0
- package/src/use-query.ts +165 -0
- package/src/use-realtime.ts +161 -0
- package/src/use-rpc.ts +109 -0
- package/src/use-storage.ts +257 -0
- package/src/use-users.ts +191 -0
- package/tsconfig.json +24 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +11 -0
- package/typedoc.json +35 -0
package/README-ADMIN.md
ADDED
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
# Fluxbase React Admin Hooks
|
|
2
|
+
|
|
3
|
+
Comprehensive React hooks for building admin dashboards and management interfaces with Fluxbase.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Quick Start](#quick-start)
|
|
9
|
+
- [Admin Authentication](#admin-authentication)
|
|
10
|
+
- [User Management](#user-management)
|
|
11
|
+
- [API Keys](#api-keys)
|
|
12
|
+
- [Webhooks](#webhooks)
|
|
13
|
+
- [Settings Management](#settings-management)
|
|
14
|
+
- [Complete Examples](#complete-examples)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @fluxbase/sdk @fluxbase/sdk-react @tanstack/react-query
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { createClient } from '@fluxbase/sdk'
|
|
26
|
+
import { FluxbaseProvider } from '@fluxbase/sdk-react'
|
|
27
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
28
|
+
import AdminDashboard from './AdminDashboard'
|
|
29
|
+
|
|
30
|
+
const client = createClient({ url: 'http://localhost:8080' })
|
|
31
|
+
const queryClient = new QueryClient()
|
|
32
|
+
|
|
33
|
+
function App() {
|
|
34
|
+
return (
|
|
35
|
+
<QueryClientProvider client={queryClient}>
|
|
36
|
+
<FluxbaseProvider client={client}>
|
|
37
|
+
<AdminDashboard />
|
|
38
|
+
</FluxbaseProvider>
|
|
39
|
+
</QueryClientProvider>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Admin Authentication
|
|
45
|
+
|
|
46
|
+
### useAdminAuth
|
|
47
|
+
|
|
48
|
+
Hook for managing admin authentication state.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { useAdminAuth } from '@fluxbase/sdk-react'
|
|
52
|
+
|
|
53
|
+
function AdminLogin() {
|
|
54
|
+
const { user, isAuthenticated, isLoading, error, login, logout } = useAdminAuth({
|
|
55
|
+
autoCheck: true // Automatically check if admin is authenticated on mount
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const [email, setEmail] = useState('')
|
|
59
|
+
const [password, setPassword] = useState('')
|
|
60
|
+
|
|
61
|
+
const handleLogin = async (e: React.FormEvent) => {
|
|
62
|
+
e.preventDefault()
|
|
63
|
+
try {
|
|
64
|
+
await login(email, password)
|
|
65
|
+
// Redirect to admin dashboard
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error('Login failed:', err)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isLoading) {
|
|
72
|
+
return <div>Checking authentication...</div>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isAuthenticated) {
|
|
76
|
+
return (
|
|
77
|
+
<div>
|
|
78
|
+
<p>Logged in as: {user?.email}</p>
|
|
79
|
+
<p>Role: {user?.role}</p>
|
|
80
|
+
<button onClick={logout}>Logout</button>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<form onSubmit={handleLogin}>
|
|
87
|
+
<input
|
|
88
|
+
type="email"
|
|
89
|
+
value={email}
|
|
90
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
91
|
+
placeholder="Admin email"
|
|
92
|
+
required
|
|
93
|
+
/>
|
|
94
|
+
<input
|
|
95
|
+
type="password"
|
|
96
|
+
value={password}
|
|
97
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
98
|
+
placeholder="Password"
|
|
99
|
+
required
|
|
100
|
+
/>
|
|
101
|
+
<button type="submit">Login</button>
|
|
102
|
+
{error && <p className="error">{error.message}</p>}
|
|
103
|
+
</form>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Protected Admin Routes
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { useAdminAuth } from '@fluxbase/sdk-react'
|
|
112
|
+
import { Navigate } from 'react-router-dom'
|
|
113
|
+
|
|
114
|
+
function ProtectedAdminRoute({ children }: { children: React.ReactNode }) {
|
|
115
|
+
const { isAuthenticated, isLoading } = useAdminAuth({ autoCheck: true })
|
|
116
|
+
|
|
117
|
+
if (isLoading) {
|
|
118
|
+
return <div>Loading...</div>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!isAuthenticated) {
|
|
122
|
+
return <Navigate to="/admin/login" replace />
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return <>{children}</>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Usage
|
|
129
|
+
<Routes>
|
|
130
|
+
<Route path="/admin/login" element={<AdminLogin />} />
|
|
131
|
+
<Route
|
|
132
|
+
path="/admin/*"
|
|
133
|
+
element={
|
|
134
|
+
<ProtectedAdminRoute>
|
|
135
|
+
<AdminDashboard />
|
|
136
|
+
</ProtectedAdminRoute>
|
|
137
|
+
}
|
|
138
|
+
/>
|
|
139
|
+
</Routes>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## User Management
|
|
143
|
+
|
|
144
|
+
### useUsers
|
|
145
|
+
|
|
146
|
+
Hook for managing users with pagination and CRUD operations.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { useUsers } from '@fluxbase/sdk-react'
|
|
150
|
+
import { useState } from 'react'
|
|
151
|
+
|
|
152
|
+
function UserManagement() {
|
|
153
|
+
const [page, setPage] = useState(0)
|
|
154
|
+
const limit = 20
|
|
155
|
+
|
|
156
|
+
const {
|
|
157
|
+
users,
|
|
158
|
+
total,
|
|
159
|
+
isLoading,
|
|
160
|
+
error,
|
|
161
|
+
refetch,
|
|
162
|
+
inviteUser,
|
|
163
|
+
updateUserRole,
|
|
164
|
+
deleteUser,
|
|
165
|
+
resetPassword
|
|
166
|
+
} = useUsers({
|
|
167
|
+
autoFetch: true,
|
|
168
|
+
limit,
|
|
169
|
+
offset: page * limit
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const handleInvite = async () => {
|
|
173
|
+
const email = prompt('Enter email:')
|
|
174
|
+
const role = confirm('Admin role?') ? 'admin' : 'user'
|
|
175
|
+
if (email) {
|
|
176
|
+
await inviteUser(email, role)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const handleRoleChange = async (userId: string, currentRole: string) => {
|
|
181
|
+
const newRole = currentRole === 'admin' ? 'user' : 'admin'
|
|
182
|
+
await updateUserRole(userId, newRole)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const handleDelete = async (userId: string) => {
|
|
186
|
+
if (confirm('Delete this user?')) {
|
|
187
|
+
await deleteUser(userId)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const handleResetPassword = async (userId: string) => {
|
|
192
|
+
const newPassword = await resetPassword(userId)
|
|
193
|
+
alert(`New password: ${newPassword}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (isLoading) return <div>Loading users...</div>
|
|
197
|
+
if (error) return <div>Error: {error.message}</div>
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div>
|
|
201
|
+
<div className="header">
|
|
202
|
+
<h2>User Management ({total} users)</h2>
|
|
203
|
+
<button onClick={handleInvite}>Invite User</button>
|
|
204
|
+
<button onClick={refetch}>Refresh</button>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<table>
|
|
208
|
+
<thead>
|
|
209
|
+
<tr>
|
|
210
|
+
<th>Email</th>
|
|
211
|
+
<th>Role</th>
|
|
212
|
+
<th>Status</th>
|
|
213
|
+
<th>Created</th>
|
|
214
|
+
<th>Actions</th>
|
|
215
|
+
</tr>
|
|
216
|
+
</thead>
|
|
217
|
+
<tbody>
|
|
218
|
+
{users.map((user) => (
|
|
219
|
+
<tr key={user.id}>
|
|
220
|
+
<td>{user.email}</td>
|
|
221
|
+
<td>
|
|
222
|
+
<span className={`badge ${user.role}`}>{user.role}</span>
|
|
223
|
+
</td>
|
|
224
|
+
<td>
|
|
225
|
+
<span className={`status ${user.email_confirmed ? 'confirmed' : 'pending'}`}>
|
|
226
|
+
{user.email_confirmed ? 'Confirmed' : 'Pending'}
|
|
227
|
+
</span>
|
|
228
|
+
</td>
|
|
229
|
+
<td>{new Date(user.created_at).toLocaleDateString()}</td>
|
|
230
|
+
<td>
|
|
231
|
+
<button onClick={() => handleRoleChange(user.id, user.role)}>
|
|
232
|
+
Toggle Role
|
|
233
|
+
</button>
|
|
234
|
+
<button onClick={() => handleResetPassword(user.id)}>
|
|
235
|
+
Reset Password
|
|
236
|
+
</button>
|
|
237
|
+
<button onClick={() => handleDelete(user.id)}>Delete</button>
|
|
238
|
+
</td>
|
|
239
|
+
</tr>
|
|
240
|
+
))}
|
|
241
|
+
</tbody>
|
|
242
|
+
</table>
|
|
243
|
+
|
|
244
|
+
<div className="pagination">
|
|
245
|
+
<button disabled={page === 0} onClick={() => setPage(page - 1)}>
|
|
246
|
+
Previous
|
|
247
|
+
</button>
|
|
248
|
+
<span>
|
|
249
|
+
Page {page + 1} of {Math.ceil(total / limit)}
|
|
250
|
+
</span>
|
|
251
|
+
<button
|
|
252
|
+
disabled={(page + 1) * limit >= total}
|
|
253
|
+
onClick={() => setPage(page + 1)}
|
|
254
|
+
>
|
|
255
|
+
Next
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### User Search and Filters
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
import { useUsers } from '@fluxbase/sdk-react'
|
|
267
|
+
import { useState, useEffect } from 'react'
|
|
268
|
+
|
|
269
|
+
function UserSearch() {
|
|
270
|
+
const [searchEmail, setSearchEmail] = useState('')
|
|
271
|
+
const [roleFilter, setRoleFilter] = useState<'admin' | 'user' | undefined>()
|
|
272
|
+
|
|
273
|
+
const { users, isLoading, refetch } = useUsers({
|
|
274
|
+
autoFetch: true,
|
|
275
|
+
email: searchEmail || undefined,
|
|
276
|
+
role: roleFilter
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// Refetch when filters change
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
refetch()
|
|
282
|
+
}, [searchEmail, roleFilter, refetch])
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div>
|
|
286
|
+
<input
|
|
287
|
+
type="text"
|
|
288
|
+
placeholder="Search by email..."
|
|
289
|
+
value={searchEmail}
|
|
290
|
+
onChange={(e) => setSearchEmail(e.target.value)}
|
|
291
|
+
/>
|
|
292
|
+
<select value={roleFilter || ''} onChange={(e) => setRoleFilter(e.target.value as any)}>
|
|
293
|
+
<option value="">All Roles</option>
|
|
294
|
+
<option value="admin">Admin</option>
|
|
295
|
+
<option value="user">User</option>
|
|
296
|
+
</select>
|
|
297
|
+
|
|
298
|
+
{isLoading ? (
|
|
299
|
+
<div>Searching...</div>
|
|
300
|
+
) : (
|
|
301
|
+
<ul>
|
|
302
|
+
{users.map((user) => (
|
|
303
|
+
<li key={user.id}>
|
|
304
|
+
{user.email} - {user.role}
|
|
305
|
+
</li>
|
|
306
|
+
))}
|
|
307
|
+
</ul>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## API Keys
|
|
315
|
+
|
|
316
|
+
### useAPIKeys
|
|
317
|
+
|
|
318
|
+
Hook for managing API keys.
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
import { useAPIKeys } from '@fluxbase/sdk-react'
|
|
322
|
+
import { useState } from 'react'
|
|
323
|
+
|
|
324
|
+
function APIKeyManagement() {
|
|
325
|
+
const { keys, isLoading, error, createKey, updateKey, revokeKey, deleteKey } = useAPIKeys({
|
|
326
|
+
autoFetch: true
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
const [showCreateForm, setShowCreateForm] = useState(false)
|
|
330
|
+
const [newKeyData, setNewKeyData] = useState<{
|
|
331
|
+
name: string
|
|
332
|
+
description: string
|
|
333
|
+
expiresInDays: number
|
|
334
|
+
}>({ name: '', description: '', expiresInDays: 365 })
|
|
335
|
+
|
|
336
|
+
const handleCreate = async (e: React.FormEvent) => {
|
|
337
|
+
e.preventDefault()
|
|
338
|
+
try {
|
|
339
|
+
const expiresAt = new Date()
|
|
340
|
+
expiresAt.setDate(expiresAt.getDate() + newKeyData.expiresInDays)
|
|
341
|
+
|
|
342
|
+
const result = await createKey({
|
|
343
|
+
name: newKeyData.name,
|
|
344
|
+
description: newKeyData.description,
|
|
345
|
+
expires_at: expiresAt.toISOString()
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// Show the key (only time it's visible)
|
|
349
|
+
alert(`API Key created!\n\nKey: ${result.key}\n\nSave this securely - it won't be shown again!`)
|
|
350
|
+
|
|
351
|
+
setShowCreateForm(false)
|
|
352
|
+
setNewKeyData({ name: '', description: '', expiresInDays: 365 })
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.error('Failed to create key:', err)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const handleRevoke = async (keyId: string) => {
|
|
359
|
+
if (confirm('Revoke this API key? It will immediately stop working.')) {
|
|
360
|
+
await revokeKey(keyId)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const handleDelete = async (keyId: string) => {
|
|
365
|
+
if (confirm('Permanently delete this API key?')) {
|
|
366
|
+
await deleteKey(keyId)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (isLoading) return <div>Loading API keys...</div>
|
|
371
|
+
if (error) return <div>Error: {error.message}</div>
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<div>
|
|
375
|
+
<div className="header">
|
|
376
|
+
<h2>API Keys ({keys.length})</h2>
|
|
377
|
+
<button onClick={() => setShowCreateForm(!showCreateForm)}>
|
|
378
|
+
{showCreateForm ? 'Cancel' : 'Create New Key'}
|
|
379
|
+
</button>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{showCreateForm && (
|
|
383
|
+
<form onSubmit={handleCreate} className="create-form">
|
|
384
|
+
<h3>Create New API Key</h3>
|
|
385
|
+
<input
|
|
386
|
+
type="text"
|
|
387
|
+
placeholder="Key name (e.g., Backend Service)"
|
|
388
|
+
value={newKeyData.name}
|
|
389
|
+
onChange={(e) => setNewKeyData({ ...newKeyData, name: e.target.value })}
|
|
390
|
+
required
|
|
391
|
+
/>
|
|
392
|
+
<textarea
|
|
393
|
+
placeholder="Description (optional)"
|
|
394
|
+
value={newKeyData.description}
|
|
395
|
+
onChange={(e) => setNewKeyData({ ...newKeyData, description: e.target.value })}
|
|
396
|
+
/>
|
|
397
|
+
<label>
|
|
398
|
+
Expires in:
|
|
399
|
+
<input
|
|
400
|
+
type="number"
|
|
401
|
+
min="1"
|
|
402
|
+
max="3650"
|
|
403
|
+
value={newKeyData.expiresInDays}
|
|
404
|
+
onChange={(e) => setNewKeyData({ ...newKeyData, expiresInDays: parseInt(e.target.value) })}
|
|
405
|
+
/>
|
|
406
|
+
days
|
|
407
|
+
</label>
|
|
408
|
+
<button type="submit">Create Key</button>
|
|
409
|
+
</form>
|
|
410
|
+
)}
|
|
411
|
+
|
|
412
|
+
<table>
|
|
413
|
+
<thead>
|
|
414
|
+
<tr>
|
|
415
|
+
<th>Name</th>
|
|
416
|
+
<th>Description</th>
|
|
417
|
+
<th>Created</th>
|
|
418
|
+
<th>Expires</th>
|
|
419
|
+
<th>Actions</th>
|
|
420
|
+
</tr>
|
|
421
|
+
</thead>
|
|
422
|
+
<tbody>
|
|
423
|
+
{keys.map((key) => (
|
|
424
|
+
<tr key={key.id}>
|
|
425
|
+
<td>{key.name}</td>
|
|
426
|
+
<td>{key.description}</td>
|
|
427
|
+
<td>{new Date(key.created_at).toLocaleDateString()}</td>
|
|
428
|
+
<td>
|
|
429
|
+
{key.expires_at ? (
|
|
430
|
+
<span className={new Date(key.expires_at) < new Date() ? 'expired' : ''}>
|
|
431
|
+
{new Date(key.expires_at).toLocaleDateString()}
|
|
432
|
+
</span>
|
|
433
|
+
) : (
|
|
434
|
+
'Never'
|
|
435
|
+
)}
|
|
436
|
+
</td>
|
|
437
|
+
<td>
|
|
438
|
+
<button onClick={() => handleRevoke(key.id)}>Revoke</button>
|
|
439
|
+
<button onClick={() => handleDelete(key.id)}>Delete</button>
|
|
440
|
+
</td>
|
|
441
|
+
</tr>
|
|
442
|
+
))}
|
|
443
|
+
</tbody>
|
|
444
|
+
</table>
|
|
445
|
+
</div>
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## Webhooks
|
|
451
|
+
|
|
452
|
+
### useWebhooks
|
|
453
|
+
|
|
454
|
+
Hook for managing webhooks and monitoring deliveries.
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
import { useWebhooks } from '@fluxbase/sdk-react'
|
|
458
|
+
import { useState } from 'react'
|
|
459
|
+
|
|
460
|
+
function WebhookManagement() {
|
|
461
|
+
const {
|
|
462
|
+
webhooks,
|
|
463
|
+
isLoading,
|
|
464
|
+
error,
|
|
465
|
+
createWebhook,
|
|
466
|
+
updateWebhook,
|
|
467
|
+
deleteWebhook,
|
|
468
|
+
testWebhook,
|
|
469
|
+
getDeliveries,
|
|
470
|
+
retryDelivery
|
|
471
|
+
} = useWebhooks({ autoFetch: true })
|
|
472
|
+
|
|
473
|
+
const [showCreateForm, setShowCreateForm] = useState(false)
|
|
474
|
+
const [selectedWebhook, setSelectedWebhook] = useState<string | null>(null)
|
|
475
|
+
const [deliveries, setDeliveries] = useState<any[]>([])
|
|
476
|
+
|
|
477
|
+
const handleCreate = async (e: React.FormEvent) => {
|
|
478
|
+
e.preventDefault()
|
|
479
|
+
const formData = new FormData(e.target as HTMLFormElement)
|
|
480
|
+
|
|
481
|
+
await createWebhook({
|
|
482
|
+
name: formData.get('name') as string,
|
|
483
|
+
url: formData.get('url') as string,
|
|
484
|
+
events: ['INSERT', 'UPDATE', 'DELETE'],
|
|
485
|
+
table: formData.get('table') as string,
|
|
486
|
+
schema: 'public',
|
|
487
|
+
enabled: true,
|
|
488
|
+
secret: `webhook_secret_${Math.random().toString(36).substring(7)}`
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
setShowCreateForm(false)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const handleTest = async (webhookId: string) => {
|
|
495
|
+
try {
|
|
496
|
+
await testWebhook(webhookId)
|
|
497
|
+
alert('Test webhook sent! Check your endpoint.')
|
|
498
|
+
} catch (err) {
|
|
499
|
+
alert('Test failed: ' + (err as Error).message)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const handleToggle = async (webhookId: string, currentlyEnabled: boolean) => {
|
|
504
|
+
await updateWebhook(webhookId, { enabled: !currentlyEnabled })
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const viewDeliveries = async (webhookId: string) => {
|
|
508
|
+
const result = await getDeliveries(webhookId, { limit: 20 })
|
|
509
|
+
setDeliveries(result.deliveries)
|
|
510
|
+
setSelectedWebhook(webhookId)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (isLoading) return <div>Loading webhooks...</div>
|
|
514
|
+
if (error) return <div>Error: {error.message}</div>
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<div>
|
|
518
|
+
<div className="header">
|
|
519
|
+
<h2>Webhooks ({webhooks.length})</h2>
|
|
520
|
+
<button onClick={() => setShowCreateForm(!showCreateForm)}>
|
|
521
|
+
{showCreateForm ? 'Cancel' : 'Create Webhook'}
|
|
522
|
+
</button>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
{showCreateForm && (
|
|
526
|
+
<form onSubmit={handleCreate} className="create-form">
|
|
527
|
+
<h3>Create New Webhook</h3>
|
|
528
|
+
<input name="name" placeholder="Webhook name" required />
|
|
529
|
+
<input name="url" type="url" placeholder="https://example.com/webhook" required />
|
|
530
|
+
<input name="table" placeholder="Table name (e.g., users)" required />
|
|
531
|
+
<button type="submit">Create</button>
|
|
532
|
+
</form>
|
|
533
|
+
)}
|
|
534
|
+
|
|
535
|
+
<table>
|
|
536
|
+
<thead>
|
|
537
|
+
<tr>
|
|
538
|
+
<th>Name</th>
|
|
539
|
+
<th>URL</th>
|
|
540
|
+
<th>Table</th>
|
|
541
|
+
<th>Events</th>
|
|
542
|
+
<th>Status</th>
|
|
543
|
+
<th>Actions</th>
|
|
544
|
+
</tr>
|
|
545
|
+
</thead>
|
|
546
|
+
<tbody>
|
|
547
|
+
{webhooks.map((webhook) => (
|
|
548
|
+
<tr key={webhook.id}>
|
|
549
|
+
<td>{webhook.name}</td>
|
|
550
|
+
<td className="url">{webhook.url}</td>
|
|
551
|
+
<td>{webhook.schema}.{webhook.table}</td>
|
|
552
|
+
<td>{webhook.events.join(', ')}</td>
|
|
553
|
+
<td>
|
|
554
|
+
<span className={`status ${webhook.enabled ? 'enabled' : 'disabled'}`}>
|
|
555
|
+
{webhook.enabled ? 'Enabled' : 'Disabled'}
|
|
556
|
+
</span>
|
|
557
|
+
</td>
|
|
558
|
+
<td>
|
|
559
|
+
<button onClick={() => handleToggle(webhook.id, webhook.enabled)}>
|
|
560
|
+
{webhook.enabled ? 'Disable' : 'Enable'}
|
|
561
|
+
</button>
|
|
562
|
+
<button onClick={() => handleTest(webhook.id)}>Test</button>
|
|
563
|
+
<button onClick={() => viewDeliveries(webhook.id)}>Deliveries</button>
|
|
564
|
+
<button onClick={() => deleteWebhook(webhook.id)}>Delete</button>
|
|
565
|
+
</td>
|
|
566
|
+
</tr>
|
|
567
|
+
))}
|
|
568
|
+
</tbody>
|
|
569
|
+
</table>
|
|
570
|
+
|
|
571
|
+
{selectedWebhook && (
|
|
572
|
+
<div className="deliveries">
|
|
573
|
+
<h3>Recent Deliveries</h3>
|
|
574
|
+
<button onClick={() => setSelectedWebhook(null)}>Close</button>
|
|
575
|
+
<table>
|
|
576
|
+
<thead>
|
|
577
|
+
<tr>
|
|
578
|
+
<th>Status</th>
|
|
579
|
+
<th>Response Code</th>
|
|
580
|
+
<th>Attempt</th>
|
|
581
|
+
<th>Created</th>
|
|
582
|
+
<th>Actions</th>
|
|
583
|
+
</tr>
|
|
584
|
+
</thead>
|
|
585
|
+
<tbody>
|
|
586
|
+
{deliveries.map((delivery) => (
|
|
587
|
+
<tr key={delivery.id}>
|
|
588
|
+
<td>
|
|
589
|
+
<span className={`status ${delivery.status}`}>{delivery.status}</span>
|
|
590
|
+
</td>
|
|
591
|
+
<td>{delivery.response_status_code || 'N/A'}</td>
|
|
592
|
+
<td>{delivery.attempt_count}</td>
|
|
593
|
+
<td>{new Date(delivery.created_at).toLocaleString()}</td>
|
|
594
|
+
<td>
|
|
595
|
+
{delivery.status === 'failed' && (
|
|
596
|
+
<button onClick={() => retryDelivery(selectedWebhook, delivery.id)}>
|
|
597
|
+
Retry
|
|
598
|
+
</button>
|
|
599
|
+
)}
|
|
600
|
+
</td>
|
|
601
|
+
</tr>
|
|
602
|
+
))}
|
|
603
|
+
</tbody>
|
|
604
|
+
</table>
|
|
605
|
+
</div>
|
|
606
|
+
)}
|
|
607
|
+
</div>
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
## Settings Management
|
|
613
|
+
|
|
614
|
+
### useAppSettings
|
|
615
|
+
|
|
616
|
+
Hook for managing application-wide settings.
|
|
617
|
+
|
|
618
|
+
```tsx
|
|
619
|
+
import { useAppSettings } from '@fluxbase/sdk-react'
|
|
620
|
+
|
|
621
|
+
function AppSettingsPanel() {
|
|
622
|
+
const { settings, isLoading, error, updateSettings } = useAppSettings({
|
|
623
|
+
autoFetch: true
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
const handleToggleFeature = async (feature: string, enabled: boolean) => {
|
|
627
|
+
await updateSettings({
|
|
628
|
+
features: {
|
|
629
|
+
...settings?.features,
|
|
630
|
+
[feature]: enabled
|
|
631
|
+
}
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const handleUpdateSecurity = async (e: React.FormEvent) => {
|
|
636
|
+
e.preventDefault()
|
|
637
|
+
const formData = new FormData(e.target as HTMLFormElement)
|
|
638
|
+
|
|
639
|
+
await updateSettings({
|
|
640
|
+
security: {
|
|
641
|
+
enable_rate_limiting: formData.get('rateLimiting') === 'on',
|
|
642
|
+
rate_limit_requests_per_minute: parseInt(formData.get('rateLimit') as string)
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (isLoading) return <div>Loading settings...</div>
|
|
648
|
+
if (error) return <div>Error: {error.message}</div>
|
|
649
|
+
if (!settings) return <div>No settings found</div>
|
|
650
|
+
|
|
651
|
+
return (
|
|
652
|
+
<div>
|
|
653
|
+
<h2>Application Settings</h2>
|
|
654
|
+
|
|
655
|
+
<section>
|
|
656
|
+
<h3>Features</h3>
|
|
657
|
+
<label>
|
|
658
|
+
<input
|
|
659
|
+
type="checkbox"
|
|
660
|
+
checked={settings.features?.enable_realtime ?? false}
|
|
661
|
+
onChange={(e) => handleToggleFeature('enable_realtime', e.target.checked)}
|
|
662
|
+
/>
|
|
663
|
+
Enable Realtime
|
|
664
|
+
</label>
|
|
665
|
+
<label>
|
|
666
|
+
<input
|
|
667
|
+
type="checkbox"
|
|
668
|
+
checked={settings.features?.enable_storage ?? false}
|
|
669
|
+
onChange={(e) => handleToggleFeature('enable_storage', e.target.checked)}
|
|
670
|
+
/>
|
|
671
|
+
Enable Storage
|
|
672
|
+
</label>
|
|
673
|
+
<label>
|
|
674
|
+
<input
|
|
675
|
+
type="checkbox"
|
|
676
|
+
checked={settings.features?.enable_functions ?? false}
|
|
677
|
+
onChange={(e) => handleToggleFeature('enable_functions', e.target.checked)}
|
|
678
|
+
/>
|
|
679
|
+
Enable Functions
|
|
680
|
+
</label>
|
|
681
|
+
</section>
|
|
682
|
+
|
|
683
|
+
<section>
|
|
684
|
+
<h3>Security</h3>
|
|
685
|
+
<form onSubmit={handleUpdateSecurity}>
|
|
686
|
+
<label>
|
|
687
|
+
<input
|
|
688
|
+
type="checkbox"
|
|
689
|
+
name="rateLimiting"
|
|
690
|
+
defaultChecked={settings.security?.enable_rate_limiting ?? false}
|
|
691
|
+
/>
|
|
692
|
+
Enable Rate Limiting
|
|
693
|
+
</label>
|
|
694
|
+
<label>
|
|
695
|
+
Requests per minute:
|
|
696
|
+
<input
|
|
697
|
+
type="number"
|
|
698
|
+
name="rateLimit"
|
|
699
|
+
defaultValue={settings.security?.rate_limit_requests_per_minute ?? 60}
|
|
700
|
+
min="1"
|
|
701
|
+
/>
|
|
702
|
+
</label>
|
|
703
|
+
<button type="submit">Update Security</button>
|
|
704
|
+
</form>
|
|
705
|
+
</section>
|
|
706
|
+
</div>
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### useSystemSettings
|
|
712
|
+
|
|
713
|
+
Hook for managing system-wide key-value settings.
|
|
714
|
+
|
|
715
|
+
```tsx
|
|
716
|
+
import { useSystemSettings } from '@fluxbase/sdk-react'
|
|
717
|
+
import { useState } from 'react'
|
|
718
|
+
|
|
719
|
+
function SystemSettingsPanel() {
|
|
720
|
+
const { settings, isLoading, error, getSetting, updateSetting, deleteSetting } = useSystemSettings({
|
|
721
|
+
autoFetch: true
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
const [editingKey, setEditingKey] = useState<string | null>(null)
|
|
725
|
+
const [editValue, setEditValue] = useState('')
|
|
726
|
+
|
|
727
|
+
const handleEdit = (key: string, currentValue: any) => {
|
|
728
|
+
setEditingKey(key)
|
|
729
|
+
setEditValue(JSON.stringify(currentValue, null, 2))
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const handleSave = async () => {
|
|
733
|
+
if (!editingKey) return
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
const parsedValue = JSON.parse(editValue)
|
|
737
|
+
await updateSetting(editingKey, { value: parsedValue })
|
|
738
|
+
setEditingKey(null)
|
|
739
|
+
} catch (err) {
|
|
740
|
+
alert('Invalid JSON')
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const handleCreate = async () => {
|
|
745
|
+
const key = prompt('Setting key:')
|
|
746
|
+
const valueStr = prompt('Setting value (JSON):')
|
|
747
|
+
const description = prompt('Description:')
|
|
748
|
+
|
|
749
|
+
if (key && valueStr) {
|
|
750
|
+
try {
|
|
751
|
+
const value = JSON.parse(valueStr)
|
|
752
|
+
await updateSetting(key, { value, description: description || undefined })
|
|
753
|
+
} catch (err) {
|
|
754
|
+
alert('Invalid JSON')
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const handleDelete = async (key: string) => {
|
|
760
|
+
if (confirm(`Delete setting "${key}"?`)) {
|
|
761
|
+
await deleteSetting(key)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (isLoading) return <div>Loading settings...</div>
|
|
766
|
+
if (error) return <div>Error: {error.message}</div>
|
|
767
|
+
|
|
768
|
+
return (
|
|
769
|
+
<div>
|
|
770
|
+
<div className="header">
|
|
771
|
+
<h2>System Settings ({settings.length})</h2>
|
|
772
|
+
<button onClick={handleCreate}>Create Setting</button>
|
|
773
|
+
</div>
|
|
774
|
+
|
|
775
|
+
<table>
|
|
776
|
+
<thead>
|
|
777
|
+
<tr>
|
|
778
|
+
<th>Key</th>
|
|
779
|
+
<th>Value</th>
|
|
780
|
+
<th>Description</th>
|
|
781
|
+
<th>Actions</th>
|
|
782
|
+
</tr>
|
|
783
|
+
</thead>
|
|
784
|
+
<tbody>
|
|
785
|
+
{settings.map((setting) => (
|
|
786
|
+
<tr key={setting.key}>
|
|
787
|
+
<td><code>{setting.key}</code></td>
|
|
788
|
+
<td>
|
|
789
|
+
{editingKey === setting.key ? (
|
|
790
|
+
<textarea
|
|
791
|
+
value={editValue}
|
|
792
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
793
|
+
rows={5}
|
|
794
|
+
/>
|
|
795
|
+
) : (
|
|
796
|
+
<pre>{JSON.stringify(setting.value, null, 2)}</pre>
|
|
797
|
+
)}
|
|
798
|
+
</td>
|
|
799
|
+
<td>{setting.description}</td>
|
|
800
|
+
<td>
|
|
801
|
+
{editingKey === setting.key ? (
|
|
802
|
+
<>
|
|
803
|
+
<button onClick={handleSave}>Save</button>
|
|
804
|
+
<button onClick={() => setEditingKey(null)}>Cancel</button>
|
|
805
|
+
</>
|
|
806
|
+
) : (
|
|
807
|
+
<>
|
|
808
|
+
<button onClick={() => handleEdit(setting.key, setting.value)}>Edit</button>
|
|
809
|
+
<button onClick={() => handleDelete(setting.key)}>Delete</button>
|
|
810
|
+
</>
|
|
811
|
+
)}
|
|
812
|
+
</td>
|
|
813
|
+
</tr>
|
|
814
|
+
))}
|
|
815
|
+
</tbody>
|
|
816
|
+
</table>
|
|
817
|
+
</div>
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
## Complete Examples
|
|
823
|
+
|
|
824
|
+
### Full Admin Dashboard
|
|
825
|
+
|
|
826
|
+
```tsx
|
|
827
|
+
import { useAdminAuth, useUsers, useAPIKeys, useWebhooks, useAppSettings } from '@fluxbase/sdk-react'
|
|
828
|
+
import { useState } from 'react'
|
|
829
|
+
|
|
830
|
+
function AdminDashboard() {
|
|
831
|
+
const { user, isAuthenticated, logout } = useAdminAuth({ autoCheck: true })
|
|
832
|
+
const { users, total: totalUsers } = useUsers({ autoFetch: true, limit: 5 })
|
|
833
|
+
const { keys } = useAPIKeys({ autoFetch: true })
|
|
834
|
+
const { webhooks } = useWebhooks({ autoFetch: true })
|
|
835
|
+
const { settings } = useAppSettings({ autoFetch: true })
|
|
836
|
+
|
|
837
|
+
const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'keys' | 'webhooks' | 'settings'>('overview')
|
|
838
|
+
|
|
839
|
+
if (!isAuthenticated) {
|
|
840
|
+
return <div>Please log in to access admin dashboard</div>
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return (
|
|
844
|
+
<div className="admin-dashboard">
|
|
845
|
+
<header>
|
|
846
|
+
<h1>Admin Dashboard</h1>
|
|
847
|
+
<div className="user-info">
|
|
848
|
+
<span>{user?.email}</span>
|
|
849
|
+
<button onClick={logout}>Logout</button>
|
|
850
|
+
</div>
|
|
851
|
+
</header>
|
|
852
|
+
|
|
853
|
+
<nav>
|
|
854
|
+
<button onClick={() => setActiveTab('overview')}>Overview</button>
|
|
855
|
+
<button onClick={() => setActiveTab('users')}>Users</button>
|
|
856
|
+
<button onClick={() => setActiveTab('keys')}>API Keys</button>
|
|
857
|
+
<button onClick={() => setActiveTab('webhooks')}>Webhooks</button>
|
|
858
|
+
<button onClick={() => setActiveTab('settings')}>Settings</button>
|
|
859
|
+
</nav>
|
|
860
|
+
|
|
861
|
+
<main>
|
|
862
|
+
{activeTab === 'overview' && (
|
|
863
|
+
<div className="overview">
|
|
864
|
+
<h2>Overview</h2>
|
|
865
|
+
<div className="stats">
|
|
866
|
+
<div className="stat-card">
|
|
867
|
+
<h3>Total Users</h3>
|
|
868
|
+
<p className="stat-value">{totalUsers}</p>
|
|
869
|
+
</div>
|
|
870
|
+
<div className="stat-card">
|
|
871
|
+
<h3>API Keys</h3>
|
|
872
|
+
<p className="stat-value">{keys.length}</p>
|
|
873
|
+
</div>
|
|
874
|
+
<div className="stat-card">
|
|
875
|
+
<h3>Webhooks</h3>
|
|
876
|
+
<p className="stat-value">{webhooks.length}</p>
|
|
877
|
+
</div>
|
|
878
|
+
<div className="stat-card">
|
|
879
|
+
<h3>Realtime</h3>
|
|
880
|
+
<p className="stat-value">
|
|
881
|
+
{settings?.features?.enable_realtime ? 'Enabled' : 'Disabled'}
|
|
882
|
+
</p>
|
|
883
|
+
</div>
|
|
884
|
+
</div>
|
|
885
|
+
|
|
886
|
+
<div className="recent-users">
|
|
887
|
+
<h3>Recent Users</h3>
|
|
888
|
+
<ul>
|
|
889
|
+
{users.slice(0, 5).map((u) => (
|
|
890
|
+
<li key={u.id}>
|
|
891
|
+
{u.email} - {u.role}
|
|
892
|
+
</li>
|
|
893
|
+
))}
|
|
894
|
+
</ul>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
)}
|
|
898
|
+
|
|
899
|
+
{activeTab === 'users' && <UserManagement />}
|
|
900
|
+
{activeTab === 'keys' && <APIKeyManagement />}
|
|
901
|
+
{activeTab === 'webhooks' && <WebhookManagement />}
|
|
902
|
+
{activeTab === 'settings' && <AppSettingsPanel />}
|
|
903
|
+
</main>
|
|
904
|
+
</div>
|
|
905
|
+
)
|
|
906
|
+
}
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### Multi-Tab Admin Interface
|
|
910
|
+
|
|
911
|
+
```tsx
|
|
912
|
+
import { Tabs, TabList, Tab, TabPanels, TabPanel } from '@reach/tabs'
|
|
913
|
+
import {
|
|
914
|
+
useUsers,
|
|
915
|
+
useAPIKeys,
|
|
916
|
+
useWebhooks,
|
|
917
|
+
useAppSettings,
|
|
918
|
+
useSystemSettings
|
|
919
|
+
} from '@fluxbase/sdk-react'
|
|
920
|
+
|
|
921
|
+
function AdminTabs() {
|
|
922
|
+
return (
|
|
923
|
+
<Tabs>
|
|
924
|
+
<TabList>
|
|
925
|
+
<Tab>Users</Tab>
|
|
926
|
+
<Tab>API Keys</Tab>
|
|
927
|
+
<Tab>Webhooks</Tab>
|
|
928
|
+
<Tab>App Settings</Tab>
|
|
929
|
+
<Tab>System Settings</Tab>
|
|
930
|
+
</TabList>
|
|
931
|
+
|
|
932
|
+
<TabPanels>
|
|
933
|
+
<TabPanel>
|
|
934
|
+
<UserManagement />
|
|
935
|
+
</TabPanel>
|
|
936
|
+
<TabPanel>
|
|
937
|
+
<APIKeyManagement />
|
|
938
|
+
</TabPanel>
|
|
939
|
+
<TabPanel>
|
|
940
|
+
<WebhookManagement />
|
|
941
|
+
</TabPanel>
|
|
942
|
+
<TabPanel>
|
|
943
|
+
<AppSettingsPanel />
|
|
944
|
+
</TabPanel>
|
|
945
|
+
<TabPanel>
|
|
946
|
+
<SystemSettingsPanel />
|
|
947
|
+
</TabPanel>
|
|
948
|
+
</TabPanels>
|
|
949
|
+
</Tabs>
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
### Real-time Updates
|
|
955
|
+
|
|
956
|
+
All hooks support automatic refetching with the `refetchInterval` option:
|
|
957
|
+
|
|
958
|
+
```tsx
|
|
959
|
+
function LiveUserList() {
|
|
960
|
+
const { users, total } = useUsers({
|
|
961
|
+
autoFetch: true,
|
|
962
|
+
refetchInterval: 5000 // Refetch every 5 seconds
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
return (
|
|
966
|
+
<div>
|
|
967
|
+
<h2>Live Users ({total})</h2>
|
|
968
|
+
<ul>
|
|
969
|
+
{users.map((user) => (
|
|
970
|
+
<li key={user.id}>{user.email}</li>
|
|
971
|
+
))}
|
|
972
|
+
</ul>
|
|
973
|
+
<small>Updates every 5 seconds</small>
|
|
974
|
+
</div>
|
|
975
|
+
)
|
|
976
|
+
}
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
## Best Practices
|
|
980
|
+
|
|
981
|
+
### Error Handling
|
|
982
|
+
|
|
983
|
+
```tsx
|
|
984
|
+
function RobustComponent() {
|
|
985
|
+
const { users, error, isLoading, refetch } = useUsers({ autoFetch: true })
|
|
986
|
+
|
|
987
|
+
if (isLoading) {
|
|
988
|
+
return <LoadingSpinner />
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (error) {
|
|
992
|
+
return (
|
|
993
|
+
<ErrorState
|
|
994
|
+
message={error.message}
|
|
995
|
+
onRetry={refetch}
|
|
996
|
+
/>
|
|
997
|
+
)
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return <UserList users={users} />
|
|
1001
|
+
}
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### Optimistic Updates
|
|
1005
|
+
|
|
1006
|
+
All mutation functions automatically refetch data after successful operations:
|
|
1007
|
+
|
|
1008
|
+
```tsx
|
|
1009
|
+
const { users, inviteUser } = useUsers({ autoFetch: true })
|
|
1010
|
+
|
|
1011
|
+
// This will automatically refetch the user list after inviting
|
|
1012
|
+
await inviteUser('new@example.com', 'user')
|
|
1013
|
+
// users state is now updated with the new user
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
### Manual Refetch
|
|
1017
|
+
|
|
1018
|
+
```tsx
|
|
1019
|
+
const { users, refetch } = useUsers({ autoFetch: false })
|
|
1020
|
+
|
|
1021
|
+
// Manually fetch when needed
|
|
1022
|
+
useEffect(() => {
|
|
1023
|
+
refetch()
|
|
1024
|
+
}, [someCondition])
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
### Performance Optimization
|
|
1028
|
+
|
|
1029
|
+
```tsx
|
|
1030
|
+
// Don't auto-fetch on mount if data isn't immediately needed
|
|
1031
|
+
const { users, refetch } = useUsers({ autoFetch: false })
|
|
1032
|
+
|
|
1033
|
+
// Fetch only when tab is active
|
|
1034
|
+
useEffect(() => {
|
|
1035
|
+
if (isTabActive) {
|
|
1036
|
+
refetch()
|
|
1037
|
+
}
|
|
1038
|
+
}, [isTabActive, refetch])
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
## TypeScript Support
|
|
1042
|
+
|
|
1043
|
+
All hooks are fully typed with comprehensive TypeScript interfaces:
|
|
1044
|
+
|
|
1045
|
+
```tsx
|
|
1046
|
+
import type { EnrichedUser, APIKey, Webhook } from '@fluxbase/sdk-react'
|
|
1047
|
+
|
|
1048
|
+
function TypedComponent() {
|
|
1049
|
+
const { users }: { users: EnrichedUser[] } = useUsers({ autoFetch: true })
|
|
1050
|
+
const { keys }: { keys: APIKey[] } = useAPIKeys({ autoFetch: true })
|
|
1051
|
+
const { webhooks }: { webhooks: Webhook[] } = useWebhooks({ autoFetch: true })
|
|
1052
|
+
|
|
1053
|
+
// Full type safety
|
|
1054
|
+
}
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
## API Reference
|
|
1058
|
+
|
|
1059
|
+
### Common Hook Options
|
|
1060
|
+
|
|
1061
|
+
All hooks support these common options:
|
|
1062
|
+
|
|
1063
|
+
- `autoFetch?: boolean` - Automatically fetch data on component mount (default: `true`)
|
|
1064
|
+
- `refetchInterval?: number` - Automatically refetch data every N milliseconds (default: `0` - disabled)
|
|
1065
|
+
|
|
1066
|
+
### Common Hook Returns
|
|
1067
|
+
|
|
1068
|
+
All hooks return these common fields:
|
|
1069
|
+
|
|
1070
|
+
- `isLoading: boolean` - Whether data is currently being fetched
|
|
1071
|
+
- `error: Error | null` - Any error that occurred during fetch/mutation
|
|
1072
|
+
- `refetch: () => Promise<void>` - Manually trigger a data refetch
|
|
1073
|
+
|
|
1074
|
+
## License
|
|
1075
|
+
|
|
1076
|
+
MIT
|