@dev-fastn-ai/react-core 1.0.16 → 1.0.18
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/README.md +44 -1304
- package/dist/core/src/types/config.d.ts +1 -0
- package/dist/index.cjs.js +18 -18
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +18 -18
- package/dist/index.esm.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,1265 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
A powerful React library for seamlessly integrating the Fastn AI connector marketplace into your applications. This package provides a complete set of hooks and components to manage connectors, configurations, and dynamic forms with full TypeScript support.
|
|
4
|
-
|
|
5
|
-
## 🚀 Features
|
|
6
|
-
|
|
7
|
-
- **🔌 Connector Management**: Fetch, display, and manage marketplace connectors
|
|
8
|
-
- **⚙️ Configuration Handling**: Create, update, and manage connector configurations
|
|
9
|
-
- **📝 Dynamic Forms**: Render configuration forms with async field options
|
|
10
|
-
- **🔄 Real-time Updates**: Built-in caching and background refetching
|
|
11
|
-
- **🎨 Customizable**: Full styling control and component customization
|
|
12
|
-
- **📱 TypeScript First**: Complete type safety and IntelliSense support
|
|
13
|
-
- **⚡ Performance Optimized**: React Query powered with intelligent caching
|
|
14
|
-
|
|
15
|
-
## 📦 Installation
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install @fastn-ai/react-core
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### Peer Dependencies
|
|
22
|
-
|
|
23
|
-
This package requires React 18+ and React Query. Make sure you have these installed:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npm install react react-dom @tanstack/react-query
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## 🏗️ Quick Start
|
|
30
|
-
|
|
31
|
-
### 1. Setup the Provider
|
|
32
|
-
|
|
33
|
-
First, wrap your application with the `FastnProvider` and provide your configuration:
|
|
34
|
-
|
|
35
|
-
```tsx
|
|
36
|
-
import { FastnProvider } from '@fastn-ai/react-core';
|
|
37
|
-
|
|
38
|
-
function App() {
|
|
39
|
-
const fastnConfig = {
|
|
40
|
-
baseUrl: 'https://api.fastn.ai', // Optional: defaults to production
|
|
41
|
-
environment: 'LIVE', // 'LIVE' | 'DRAFT' | custom string
|
|
42
|
-
authToken: 'your-auth-token',
|
|
43
|
-
tenantId: 'your-tenant-id',
|
|
44
|
-
spaceId: 'your-space-id',
|
|
45
|
-
customAuth: false, // Optional: for custom authentication
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<FastnProvider config={fastnConfig}>
|
|
50
|
-
<YourApp />
|
|
51
|
-
</FastnProvider>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### 2. Use the Hooks
|
|
57
|
-
|
|
58
|
-
Now you can use any of the provided hooks in your components:
|
|
59
|
-
|
|
60
|
-
```tsx
|
|
61
|
-
import { useConnectors, useConfigurations } from '@fastn-ai/react-core';
|
|
62
|
-
|
|
63
|
-
function MyComponent() {
|
|
64
|
-
const { data: connectors, isLoading, error } = useConnectors();
|
|
65
|
-
|
|
66
|
-
if (isLoading) return <div>Loading connectors...</div>;
|
|
67
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div>
|
|
71
|
-
{connectors?.map(connector => (
|
|
72
|
-
<ConnectorCard key={connector.id} connector={connector} />
|
|
73
|
-
))}
|
|
74
|
-
</div>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## 📚 Detailed API Reference
|
|
80
|
-
|
|
81
|
-
### FastnProvider
|
|
82
|
-
|
|
83
|
-
The main provider component that initializes the Fastn client and provides React Query context.
|
|
84
|
-
|
|
85
|
-
#### Props
|
|
86
|
-
|
|
87
|
-
```tsx
|
|
88
|
-
interface FastnProviderProps {
|
|
89
|
-
children: React.ReactNode;
|
|
90
|
-
config: Required<FastnConfig>;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface FastnConfig {
|
|
94
|
-
baseUrl?: string;
|
|
95
|
-
environment?: FastnEnvironment;
|
|
96
|
-
authToken: string;
|
|
97
|
-
tenantId: string;
|
|
98
|
-
spaceId: string;
|
|
99
|
-
customAuth?: boolean;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
type FastnEnvironment = 'LIVE' | 'DRAFT' | string;
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
#### Example
|
|
106
|
-
|
|
107
|
-
```tsx
|
|
108
|
-
import { FastnProvider } from '@fastn-ai/react-core';
|
|
109
|
-
|
|
110
|
-
function App() {
|
|
111
|
-
const config = {
|
|
112
|
-
baseUrl: 'https://api.fastn.ai',
|
|
113
|
-
environment: 'LIVE',
|
|
114
|
-
authToken: process.env.REACT_APP_FASTN_AUTH_TOKEN!,
|
|
115
|
-
tenantId: process.env.REACT_APP_FASTN_TENANT_ID!,
|
|
116
|
-
spaceId: process.env.REACT_APP_FASTN_SPACE_ID!,
|
|
117
|
-
customAuth: false,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<FastnProvider config={config}>
|
|
122
|
-
<Router>
|
|
123
|
-
<Routes>
|
|
124
|
-
<Route path="/connectors" element={<ConnectorsPage />} />
|
|
125
|
-
<Route path="/configurations" element={<ConfigurationsPage />} />
|
|
126
|
-
</Routes>
|
|
127
|
-
</Router>
|
|
128
|
-
</FastnProvider>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### useConnectors
|
|
134
|
-
|
|
135
|
-
Fetches and manages the list of available connectors from the marketplace.
|
|
136
|
-
|
|
137
|
-
#### Usage
|
|
138
|
-
|
|
139
|
-
```tsx
|
|
140
|
-
import { useConnectors } from '@fastn-ai/react-core';
|
|
141
|
-
|
|
142
|
-
function ConnectorsList() {
|
|
143
|
-
const {
|
|
144
|
-
data: connectors,
|
|
145
|
-
isLoading,
|
|
146
|
-
error,
|
|
147
|
-
refetch
|
|
148
|
-
} = useConnectors();
|
|
149
|
-
|
|
150
|
-
if (isLoading) return <ConnectorsSkeleton />;
|
|
151
|
-
if (error) return <ErrorMessage error={error} />;
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
155
|
-
{connectors?.map(connector => (
|
|
156
|
-
<ConnectorCard
|
|
157
|
-
key={connector.id}
|
|
158
|
-
connector={connector}
|
|
159
|
-
onRefresh={refetch}
|
|
160
|
-
/>
|
|
161
|
-
))}
|
|
162
|
-
</div>
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
#### Return Value
|
|
168
|
-
|
|
169
|
-
```tsx
|
|
170
|
-
interface UseConnectorsReturn {
|
|
171
|
-
data: Connector[] | undefined;
|
|
172
|
-
isLoading: boolean;
|
|
173
|
-
isError: boolean;
|
|
174
|
-
error: Error | null;
|
|
175
|
-
refetch: () => Promise<any>;
|
|
176
|
-
isFetching: boolean;
|
|
177
|
-
isSuccess: boolean;
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
#### Connector Type
|
|
182
|
-
|
|
183
|
-
```tsx
|
|
184
|
-
interface Connector {
|
|
185
|
-
readonly id: string;
|
|
186
|
-
readonly name: string;
|
|
187
|
-
readonly description: string;
|
|
188
|
-
readonly imageUri?: string;
|
|
189
|
-
readonly status: ConnectorStatus;
|
|
190
|
-
readonly actions: readonly ConnectorAction[];
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
enum ConnectorStatus {
|
|
194
|
-
ACTIVE = "ACTIVE",
|
|
195
|
-
INACTIVE = "INACTIVE",
|
|
196
|
-
ALL = "ALL",
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
interface ConnectorAction {
|
|
200
|
-
readonly name: string;
|
|
201
|
-
readonly actionType: ConnectorActionType | string;
|
|
202
|
-
readonly form?: ConnectorForm | null;
|
|
203
|
-
readonly onClick?: (data?: unknown) => Promise<unknown>;
|
|
204
|
-
readonly onSubmit?: (formData: Record<string, unknown>) => Promise<unknown>;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
enum ConnectorActionType {
|
|
208
|
-
ACTIVATION = "ACTIVATION",
|
|
209
|
-
DEACTIVATION = "DEACTIVATION",
|
|
210
|
-
NONE = "NONE",
|
|
211
|
-
ENABLE = "ENABLE",
|
|
212
|
-
DISABLE = "DISABLE",
|
|
213
|
-
DELETE = "DELETE",
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### useConfigurations
|
|
218
|
-
|
|
219
|
-
Fetches and manages connector configurations for a specific configuration ID.
|
|
220
|
-
|
|
221
|
-
#### Usage
|
|
222
|
-
|
|
223
|
-
```tsx
|
|
224
|
-
import { useConfigurations } from '@fastn-ai/react-core';
|
|
225
|
-
|
|
226
|
-
function ConfigurationsList({ configurationId }: { configurationId: string }) {
|
|
227
|
-
const {
|
|
228
|
-
data: configurations,
|
|
229
|
-
isLoading,
|
|
230
|
-
error
|
|
231
|
-
} = useConfigurations({
|
|
232
|
-
configurationId,
|
|
233
|
-
status: 'ALL' // 'ENABLED' | 'DISABLED' | 'ALL' | 'IDLE'
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
if (isLoading) return <ConfigurationsSkeleton />;
|
|
237
|
-
if (error) return <ErrorMessage error={error} />;
|
|
238
|
-
|
|
239
|
-
return (
|
|
240
|
-
<div className="space-y-4">
|
|
241
|
-
{configurations?.map(configuration => (
|
|
242
|
-
<ConfigurationCard
|
|
243
|
-
key={configuration.id}
|
|
244
|
-
configuration={configuration}
|
|
245
|
-
/>
|
|
246
|
-
))}
|
|
247
|
-
</div>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
#### Parameters
|
|
253
|
-
|
|
254
|
-
```tsx
|
|
255
|
-
interface GetConfigurationsInput {
|
|
256
|
-
readonly configurationId: string;
|
|
257
|
-
readonly status?: "ENABLED" | "DISABLED" | "ALL" | "IDLE";
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
#### Return Value
|
|
262
|
-
|
|
263
|
-
```tsx
|
|
264
|
-
interface UseConfigurationsReturn {
|
|
265
|
-
data: Configuration[] | undefined;
|
|
266
|
-
isLoading: boolean;
|
|
267
|
-
isError: boolean;
|
|
268
|
-
error: Error | null;
|
|
269
|
-
refetch: () => Promise<any>;
|
|
270
|
-
isFetching: boolean;
|
|
271
|
-
isSuccess: boolean;
|
|
272
|
-
}
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
#### Configuration Type
|
|
276
|
-
|
|
277
|
-
```tsx
|
|
278
|
-
interface Configuration {
|
|
279
|
-
readonly id: string;
|
|
280
|
-
readonly connectorId: string;
|
|
281
|
-
readonly configurationId: string;
|
|
282
|
-
readonly name: string;
|
|
283
|
-
readonly flowId: string;
|
|
284
|
-
readonly description: string;
|
|
285
|
-
readonly imageUri: string;
|
|
286
|
-
readonly status: string;
|
|
287
|
-
readonly actions: readonly ConfigurationAction[];
|
|
288
|
-
readonly metadata?: unknown;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
interface ConfigurationAction {
|
|
292
|
-
readonly name: string;
|
|
293
|
-
readonly actionType: ConfigurationActionType;
|
|
294
|
-
readonly onClick?: () => Promise<void>;
|
|
295
|
-
readonly form?: ConnectorForm | null;
|
|
296
|
-
readonly onSubmit?: (formData: unknown) => Promise<void>;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
type ConfigurationActionType = ConnectorActionType.ENABLE | ConnectorActionType.DISABLE | ConnectorActionType.DELETE;
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
### useConfigurationForm
|
|
303
|
-
|
|
304
|
-
Fetches and manages configuration forms for setting up connectors.
|
|
305
|
-
|
|
306
|
-
#### Usage
|
|
307
|
-
|
|
308
|
-
```tsx
|
|
309
|
-
import { useConfigurationForm } from '@fastn-ai/react-core';
|
|
310
|
-
|
|
311
|
-
function ConfigurationForm({
|
|
312
|
-
configurationId,
|
|
313
|
-
connectorId,
|
|
314
|
-
configuration
|
|
315
|
-
}: {
|
|
316
|
-
configurationId: string;
|
|
317
|
-
connectorId: string;
|
|
318
|
-
configuration: Configuration;
|
|
319
|
-
}) {
|
|
320
|
-
const {
|
|
321
|
-
data: form,
|
|
322
|
-
isLoading,
|
|
323
|
-
error
|
|
324
|
-
} = useConfigurationForm({
|
|
325
|
-
configurationId,
|
|
326
|
-
connectorId,
|
|
327
|
-
configuration
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
if (isLoading) return <FormSkeleton />;
|
|
331
|
-
if (error) return <ErrorMessage error={error} />;
|
|
332
|
-
if (!form) return <div>No form available</div>;
|
|
333
|
-
|
|
334
|
-
return (
|
|
335
|
-
<ConfigurationFormRenderer
|
|
336
|
-
form={form}
|
|
337
|
-
onSubmit={handleSubmit}
|
|
338
|
-
onCancel={handleCancel}
|
|
339
|
-
/>
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
#### Parameters
|
|
345
|
-
|
|
346
|
-
```tsx
|
|
347
|
-
interface GetConfigurationFormInput {
|
|
348
|
-
readonly configurationId: string;
|
|
349
|
-
readonly connectorId: string;
|
|
350
|
-
readonly configuration: Configuration;
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
#### Return Value
|
|
355
|
-
|
|
356
|
-
```tsx
|
|
357
|
-
interface UseConfigurationFormReturn {
|
|
358
|
-
data: ConfigurationForm | undefined;
|
|
359
|
-
isLoading: boolean;
|
|
360
|
-
isError: boolean;
|
|
361
|
-
error: Error | null;
|
|
362
|
-
refetch: () => Promise<any>;
|
|
363
|
-
isFetching: boolean;
|
|
364
|
-
isSuccess: boolean;
|
|
365
|
-
}
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
#### ConfigurationForm Type
|
|
369
|
-
|
|
370
|
-
```tsx
|
|
371
|
-
interface ConfigurationForm {
|
|
372
|
-
readonly name: string;
|
|
373
|
-
readonly description: string;
|
|
374
|
-
readonly imageUri: string;
|
|
375
|
-
readonly fields: readonly ConnectorField[];
|
|
376
|
-
readonly submitButtonLabel?: string;
|
|
377
|
-
readonly loading?: boolean;
|
|
378
|
-
readonly error?: string;
|
|
379
|
-
readonly submitHandler?: (args: { formData: FormData }) => Promise<void>;
|
|
380
|
-
}
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### useFieldOptions
|
|
384
|
-
|
|
385
|
-
Manages async select field options with search, pagination, and caching.
|
|
386
|
-
|
|
387
|
-
#### Usage
|
|
388
|
-
|
|
389
|
-
```tsx
|
|
390
|
-
import { useFieldOptions } from '@fastn-ai/react-core';
|
|
391
|
-
|
|
392
|
-
function AsyncSelectField({ field }: { field: ConnectorField }) {
|
|
393
|
-
const {
|
|
394
|
-
options,
|
|
395
|
-
loading,
|
|
396
|
-
loadingMore,
|
|
397
|
-
hasNext,
|
|
398
|
-
query,
|
|
399
|
-
refresh,
|
|
400
|
-
search,
|
|
401
|
-
loadMore,
|
|
402
|
-
error
|
|
403
|
-
} = useFieldOptions(field);
|
|
404
|
-
|
|
405
|
-
return (
|
|
406
|
-
<div>
|
|
407
|
-
<AsyncSelect
|
|
408
|
-
options={options}
|
|
409
|
-
isLoading={loading}
|
|
410
|
-
onInputChange={search}
|
|
411
|
-
onMenuScrollToBottom={loadMore}
|
|
412
|
-
loadMore={loadingMore}
|
|
413
|
-
hasMore={hasNext}
|
|
414
|
-
onRefresh={refresh}
|
|
415
|
-
placeholder={field.placeholder}
|
|
416
|
-
isClearable
|
|
417
|
-
isSearchable
|
|
418
|
-
/>
|
|
419
|
-
{error && <ErrorMessage error={error} />}
|
|
420
|
-
</div>
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
#### Return Value
|
|
426
|
-
|
|
427
|
-
```tsx
|
|
428
|
-
interface UseFieldOptionsReturn {
|
|
429
|
-
options: SelectOption[];
|
|
430
|
-
loading: boolean;
|
|
431
|
-
loadingMore: boolean;
|
|
432
|
-
hasNext: boolean;
|
|
433
|
-
query: string;
|
|
434
|
-
refresh: () => Promise<void>;
|
|
435
|
-
search: (query: string) => void;
|
|
436
|
-
loadMore: () => Promise<void>;
|
|
437
|
-
totalLoadedOptions: number;
|
|
438
|
-
error: string | null;
|
|
439
|
-
}
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
#### SelectOption Type
|
|
443
|
-
|
|
444
|
-
```tsx
|
|
445
|
-
interface SelectOption {
|
|
446
|
-
readonly label: string;
|
|
447
|
-
readonly value: string;
|
|
448
|
-
}
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
## 🎨 Component Examples
|
|
452
|
-
|
|
453
|
-
### Connector Card Component
|
|
454
|
-
|
|
455
|
-
```tsx
|
|
456
|
-
import { useConnectors } from '@fastn-ai/react-core';
|
|
457
|
-
import type { Connector } from '@fastn-ai/react-core';
|
|
458
|
-
|
|
459
|
-
interface ConnectorCardProps {
|
|
460
|
-
connector: Connector;
|
|
461
|
-
onRefresh?: () => void;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function ConnectorCard({ connector, onRefresh }: ConnectorCardProps) {
|
|
465
|
-
const handleAction = async (action: ConnectorAction) => {
|
|
466
|
-
try {
|
|
467
|
-
if (action.onClick) {
|
|
468
|
-
await action.onClick();
|
|
469
|
-
onRefresh?.();
|
|
470
|
-
}
|
|
471
|
-
} catch (error) {
|
|
472
|
-
console.error('Action failed:', error);
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
return (
|
|
477
|
-
<div className="border rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow">
|
|
478
|
-
<div className="flex items-center space-x-4">
|
|
479
|
-
{connector.imageUri && (
|
|
480
|
-
<img
|
|
481
|
-
src={connector.imageUri}
|
|
482
|
-
alt={connector.name}
|
|
483
|
-
className="w-12 h-12 rounded-lg object-cover"
|
|
484
|
-
/>
|
|
485
|
-
)}
|
|
486
|
-
<div className="flex-1">
|
|
487
|
-
<h3 className="text-lg font-semibold text-gray-900">
|
|
488
|
-
{connector.name}
|
|
489
|
-
</h3>
|
|
490
|
-
<p className="text-sm text-gray-600">
|
|
491
|
-
{connector.description}
|
|
492
|
-
</p>
|
|
493
|
-
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
|
494
|
-
connector.status === 'ACTIVE'
|
|
495
|
-
? 'bg-green-100 text-green-800'
|
|
496
|
-
: 'bg-gray-100 text-gray-800'
|
|
497
|
-
}`}>
|
|
498
|
-
{connector.status}
|
|
499
|
-
</span>
|
|
500
|
-
</div>
|
|
501
|
-
</div>
|
|
502
|
-
|
|
503
|
-
<div className="mt-4 flex space-x-2">
|
|
504
|
-
{connector.actions.map((action, index) => (
|
|
505
|
-
<button
|
|
506
|
-
key={index}
|
|
507
|
-
onClick={() => handleAction(action)}
|
|
508
|
-
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
|
509
|
-
action.actionType === 'ACTIVATION' || action.actionType === 'ENABLE'
|
|
510
|
-
? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
511
|
-
: action.actionType === 'DEACTIVATION' || action.actionType === 'DISABLE'
|
|
512
|
-
? 'bg-red-600 text-white hover:bg-red-700'
|
|
513
|
-
: 'bg-gray-600 text-white hover:bg-gray-700'
|
|
514
|
-
}`}
|
|
515
|
-
>
|
|
516
|
-
{action.name}
|
|
517
|
-
</button>
|
|
518
|
-
))}
|
|
519
|
-
</div>
|
|
520
|
-
</div>
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
### Configuration Card Component
|
|
526
|
-
|
|
527
|
-
```tsx
|
|
528
|
-
import { useConfigurations } from '@fastn-ai/react-core';
|
|
529
|
-
import type { Configuration } from '@fastn-ai/react-core';
|
|
530
|
-
|
|
531
|
-
interface ConfigurationCardProps {
|
|
532
|
-
configuration: Configuration;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function ConfigurationCard({ configuration }: ConfigurationCardProps) {
|
|
536
|
-
const handleAction = async (action: ConfigurationAction) => {
|
|
537
|
-
try {
|
|
538
|
-
if (action.onClick) {
|
|
539
|
-
await action.onClick();
|
|
540
|
-
}
|
|
541
|
-
} catch (error) {
|
|
542
|
-
console.error('Configuration action failed:', error);
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
return (
|
|
547
|
-
<div className="border rounded-lg p-6 shadow-sm">
|
|
548
|
-
<div className="flex items-center space-x-4">
|
|
549
|
-
<img
|
|
550
|
-
src={configuration.imageUri}
|
|
551
|
-
alt={configuration.name}
|
|
552
|
-
className="w-12 h-12 rounded-lg object-cover"
|
|
553
|
-
/>
|
|
554
|
-
<div className="flex-1">
|
|
555
|
-
<h3 className="text-lg font-semibold text-gray-900">
|
|
556
|
-
{configuration.name}
|
|
557
|
-
</h3>
|
|
558
|
-
<p className="text-sm text-gray-600">
|
|
559
|
-
{configuration.description}
|
|
560
|
-
</p>
|
|
561
|
-
<div className="flex items-center space-x-2 mt-2">
|
|
562
|
-
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
|
563
|
-
configuration.status === 'ENABLED'
|
|
564
|
-
? 'bg-green-100 text-green-800'
|
|
565
|
-
: configuration.status === 'DISABLED'
|
|
566
|
-
? 'bg-red-100 text-red-800'
|
|
567
|
-
: 'bg-yellow-100 text-yellow-800'
|
|
568
|
-
}`}>
|
|
569
|
-
{configuration.status}
|
|
570
|
-
</span>
|
|
571
|
-
<span className="text-xs text-gray-500">
|
|
572
|
-
ID: {configuration.id}
|
|
573
|
-
</span>
|
|
574
|
-
</div>
|
|
575
|
-
</div>
|
|
576
|
-
</div>
|
|
577
|
-
|
|
578
|
-
<div className="mt-4 flex space-x-2">
|
|
579
|
-
{configuration.actions.map((action, index) => (
|
|
580
|
-
<button
|
|
581
|
-
key={index}
|
|
582
|
-
onClick={() => handleAction(action)}
|
|
583
|
-
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
|
584
|
-
action.actionType === 'ENABLE'
|
|
585
|
-
? 'bg-green-600 text-white hover:bg-green-700'
|
|
586
|
-
: action.actionType === 'DISABLE'
|
|
587
|
-
? 'bg-yellow-600 text-white hover:bg-yellow-700'
|
|
588
|
-
: action.actionType === 'DELETE'
|
|
589
|
-
? 'bg-red-600 text-white hover:bg-red-700'
|
|
590
|
-
: 'bg-gray-600 text-white hover:bg-gray-700'
|
|
591
|
-
}`}
|
|
592
|
-
>
|
|
593
|
-
{action.name}
|
|
594
|
-
</button>
|
|
595
|
-
))}
|
|
596
|
-
</div>
|
|
597
|
-
</div>
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
### Configuration Form Renderer
|
|
603
|
-
|
|
604
|
-
```tsx
|
|
605
|
-
import { useConfigurationForm, useFieldOptions } from '@fastn-ai/react-core';
|
|
606
|
-
import type { ConfigurationForm, ConnectorField } from '@fastn-ai/react-core';
|
|
607
|
-
|
|
608
|
-
interface ConfigurationFormRendererProps {
|
|
609
|
-
form: ConfigurationForm;
|
|
610
|
-
onSubmit: (formData: Record<string, any>) => Promise<void>;
|
|
611
|
-
onCancel: () => void;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function ConfigurationFormRenderer({
|
|
615
|
-
form,
|
|
616
|
-
onSubmit,
|
|
617
|
-
onCancel
|
|
618
|
-
}: ConfigurationFormRendererProps) {
|
|
619
|
-
const [formData, setFormData] = useState<Record<string, any>>({});
|
|
620
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
621
|
-
|
|
622
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
623
|
-
e.preventDefault();
|
|
624
|
-
setIsSubmitting(true);
|
|
625
|
-
|
|
626
|
-
try {
|
|
627
|
-
await onSubmit(formData);
|
|
628
|
-
} catch (error) {
|
|
629
|
-
console.error('Form submission failed:', error);
|
|
630
|
-
} finally {
|
|
631
|
-
setIsSubmitting(false);
|
|
632
|
-
}
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
const renderField = (field: ConnectorField) => {
|
|
636
|
-
switch (field.type) {
|
|
637
|
-
case 'text':
|
|
638
|
-
case 'email':
|
|
639
|
-
case 'password':
|
|
640
|
-
return (
|
|
641
|
-
<input
|
|
642
|
-
type={field.type}
|
|
643
|
-
name={field.key}
|
|
644
|
-
placeholder={field.placeholder}
|
|
645
|
-
required={field.required}
|
|
646
|
-
disabled={field.disabled}
|
|
647
|
-
value={formData[field.key] || ''}
|
|
648
|
-
onChange={(e) => setFormData(prev => ({
|
|
649
|
-
...prev,
|
|
650
|
-
[field.key]: e.target.value
|
|
651
|
-
}))}
|
|
652
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
653
|
-
/>
|
|
654
|
-
);
|
|
655
|
-
|
|
656
|
-
case 'select':
|
|
657
|
-
return <AsyncSelectField field={field} />;
|
|
658
|
-
|
|
659
|
-
case 'multi-select':
|
|
660
|
-
return <AsyncMultiSelectField field={field} />;
|
|
661
|
-
|
|
662
|
-
case 'number':
|
|
663
|
-
return (
|
|
664
|
-
<input
|
|
665
|
-
type="number"
|
|
666
|
-
name={field.key}
|
|
667
|
-
placeholder={field.placeholder}
|
|
668
|
-
required={field.required}
|
|
669
|
-
disabled={field.disabled}
|
|
670
|
-
value={formData[field.key] || ''}
|
|
671
|
-
onChange={(e) => setFormData(prev => ({
|
|
672
|
-
...prev,
|
|
673
|
-
[field.key]: e.target.value
|
|
674
|
-
}))}
|
|
675
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
676
|
-
/>
|
|
677
|
-
);
|
|
678
|
-
|
|
679
|
-
case 'checkbox':
|
|
680
|
-
return (
|
|
681
|
-
<input
|
|
682
|
-
type="checkbox"
|
|
683
|
-
name={field.key}
|
|
684
|
-
required={field.required}
|
|
685
|
-
disabled={field.disabled}
|
|
686
|
-
checked={formData[field.key] || false}
|
|
687
|
-
onChange={(e) => setFormData(prev => ({
|
|
688
|
-
...prev,
|
|
689
|
-
[field.key]: e.target.checked
|
|
690
|
-
}))}
|
|
691
|
-
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
692
|
-
/>
|
|
693
|
-
);
|
|
694
|
-
|
|
695
|
-
default:
|
|
696
|
-
return (
|
|
697
|
-
<input
|
|
698
|
-
type="text"
|
|
699
|
-
name={field.key}
|
|
700
|
-
placeholder={field.placeholder}
|
|
701
|
-
required={field.required}
|
|
702
|
-
disabled={field.disabled}
|
|
703
|
-
value={formData[field.key] || ''}
|
|
704
|
-
onChange={(e) => setFormData(prev => ({
|
|
705
|
-
...prev,
|
|
706
|
-
[field.key]: e.target.value
|
|
707
|
-
}))}
|
|
708
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
709
|
-
/>
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
return (
|
|
715
|
-
<div className="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-lg">
|
|
716
|
-
<div className="mb-6">
|
|
717
|
-
<h2 className="text-2xl font-bold text-gray-900">{form.name}</h2>
|
|
718
|
-
<p className="text-gray-600 mt-2">{form.description}</p>
|
|
719
|
-
</div>
|
|
720
|
-
|
|
721
|
-
<form onSubmit={handleSubmit} className="space-y-6">
|
|
722
|
-
{form.fields.map((field) => (
|
|
723
|
-
<div key={field.key} className="space-y-2">
|
|
724
|
-
<label className="block text-sm font-medium text-gray-700">
|
|
725
|
-
{field.label}
|
|
726
|
-
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
727
|
-
</label>
|
|
728
|
-
{renderField(field)}
|
|
729
|
-
{field.description && (
|
|
730
|
-
<p className="text-sm text-gray-500">{field.description}</p>
|
|
731
|
-
)}
|
|
732
|
-
</div>
|
|
733
|
-
))}
|
|
734
|
-
|
|
735
|
-
<div className="flex space-x-4 pt-6">
|
|
736
|
-
<button
|
|
737
|
-
type="submit"
|
|
738
|
-
disabled={isSubmitting}
|
|
739
|
-
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
740
|
-
>
|
|
741
|
-
{isSubmitting ? 'Saving...' : (form.submitButtonLabel || 'Save Configuration')}
|
|
742
|
-
</button>
|
|
743
|
-
<button
|
|
744
|
-
type="button"
|
|
745
|
-
onClick={onCancel}
|
|
746
|
-
className="flex-1 bg-gray-300 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-400"
|
|
747
|
-
>
|
|
748
|
-
Cancel
|
|
749
|
-
</button>
|
|
750
|
-
</div>
|
|
751
|
-
</form>
|
|
752
|
-
</div>
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
### Async Select Field Component
|
|
758
|
-
|
|
759
|
-
```tsx
|
|
760
|
-
import { useFieldOptions } from '@fastn-ai/react-core';
|
|
761
|
-
import type { ConnectorField, SelectOption } from '@fastn-ai/react-core';
|
|
762
|
-
|
|
763
|
-
interface AsyncSelectFieldProps {
|
|
764
|
-
field: ConnectorField;
|
|
765
|
-
value?: SelectOption | null;
|
|
766
|
-
onChange?: (option: SelectOption | null) => void;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function AsyncSelectField({ field, value, onChange }: AsyncSelectFieldProps) {
|
|
770
|
-
const {
|
|
771
|
-
options,
|
|
772
|
-
loading,
|
|
773
|
-
loadingMore,
|
|
774
|
-
hasNext,
|
|
775
|
-
query,
|
|
776
|
-
refresh,
|
|
777
|
-
search,
|
|
778
|
-
loadMore,
|
|
779
|
-
error
|
|
780
|
-
} = useFieldOptions(field);
|
|
781
|
-
|
|
782
|
-
const handleInputChange = (inputValue: string) => {
|
|
783
|
-
search(inputValue);
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
const handleMenuScrollToBottom = () => {
|
|
787
|
-
if (hasNext && !loadingMore) {
|
|
788
|
-
loadMore();
|
|
789
|
-
}
|
|
790
|
-
};
|
|
791
|
-
|
|
792
|
-
return (
|
|
793
|
-
<div className="relative">
|
|
794
|
-
<select
|
|
795
|
-
value={value?.value || ''}
|
|
796
|
-
onChange={(e) => {
|
|
797
|
-
const option = options.find(opt => opt.value === e.target.value);
|
|
798
|
-
onChange?.(option || null);
|
|
799
|
-
}}
|
|
800
|
-
disabled={loading}
|
|
801
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
802
|
-
>
|
|
803
|
-
<option value="">{field.placeholder || 'Select an option...'}</option>
|
|
804
|
-
{options.map((option) => (
|
|
805
|
-
<option key={option.value} value={option.value}>
|
|
806
|
-
{option.label}
|
|
807
|
-
</option>
|
|
808
|
-
))}
|
|
809
|
-
</select>
|
|
810
|
-
|
|
811
|
-
{loading && (
|
|
812
|
-
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
|
813
|
-
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
|
|
814
|
-
</div>
|
|
815
|
-
)}
|
|
816
|
-
|
|
817
|
-
{hasNext && (
|
|
818
|
-
<button
|
|
819
|
-
type="button"
|
|
820
|
-
onClick={loadMore}
|
|
821
|
-
disabled={loadingMore}
|
|
822
|
-
className="mt-2 text-sm text-blue-600 hover:text-blue-800 disabled:opacity-50"
|
|
823
|
-
>
|
|
824
|
-
{loadingMore ? 'Loading more...' : 'Load more options'}
|
|
825
|
-
</button>
|
|
826
|
-
)}
|
|
827
|
-
|
|
828
|
-
{error && (
|
|
829
|
-
<p className="mt-1 text-sm text-red-600">{error}</p>
|
|
830
|
-
)}
|
|
831
|
-
</div>
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
## 🔧 Advanced Usage
|
|
837
|
-
|
|
838
|
-
### Custom Error Handling
|
|
839
|
-
|
|
840
|
-
```tsx
|
|
841
|
-
import { useConnectors } from '@fastn-ai/react-core';
|
|
842
|
-
|
|
843
|
-
function ConnectorsWithErrorHandling() {
|
|
844
|
-
const { data: connectors, isLoading, error, refetch } = useConnectors();
|
|
845
|
-
|
|
846
|
-
if (error) {
|
|
847
|
-
return (
|
|
848
|
-
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
849
|
-
<div className="flex">
|
|
850
|
-
<div className="flex-shrink-0">
|
|
851
|
-
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
852
|
-
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
853
|
-
</svg>
|
|
854
|
-
</div>
|
|
855
|
-
<div className="ml-3">
|
|
856
|
-
<h3 className="text-sm font-medium text-red-800">
|
|
857
|
-
Failed to load connectors
|
|
858
|
-
</h3>
|
|
859
|
-
<div className="mt-2 text-sm text-red-700">
|
|
860
|
-
<p>{error.message}</p>
|
|
861
|
-
</div>
|
|
862
|
-
<div className="mt-4">
|
|
863
|
-
<button
|
|
864
|
-
onClick={() => refetch()}
|
|
865
|
-
className="bg-red-100 text-red-800 px-3 py-2 rounded-md text-sm font-medium hover:bg-red-200"
|
|
866
|
-
>
|
|
867
|
-
Try again
|
|
868
|
-
</button>
|
|
869
|
-
</div>
|
|
870
|
-
</div>
|
|
871
|
-
</div>
|
|
872
|
-
</div>
|
|
873
|
-
);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// ... rest of component
|
|
877
|
-
}
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
### Optimistic Updates
|
|
881
|
-
|
|
882
|
-
```tsx
|
|
883
|
-
import { useConnectors } from '@fastn-ai/react-core';
|
|
884
|
-
import { useQueryClient } from '@tanstack/react-query';
|
|
885
|
-
|
|
886
|
-
function ConnectorWithOptimisticUpdate({ connector }: { connector: Connector }) {
|
|
887
|
-
const queryClient = useQueryClient();
|
|
888
|
-
|
|
889
|
-
const handleAction = async (action: ConnectorAction) => {
|
|
890
|
-
if (!action.onClick) return;
|
|
891
|
-
|
|
892
|
-
// Optimistically update the UI
|
|
893
|
-
queryClient.setQueryData(['connectors'], (oldData: Connector[] | undefined) => {
|
|
894
|
-
if (!oldData) return oldData;
|
|
895
|
-
|
|
896
|
-
return oldData.map(conn => {
|
|
897
|
-
if (conn.id === connector.id) {
|
|
898
|
-
return {
|
|
899
|
-
...conn,
|
|
900
|
-
status: action.actionType === 'ACTIVATION' ? 'ACTIVE' : 'INACTIVE'
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
return conn;
|
|
904
|
-
});
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
try {
|
|
908
|
-
await action.onClick();
|
|
909
|
-
// Refetch to ensure data consistency
|
|
910
|
-
queryClient.invalidateQueries(['connectors']);
|
|
911
|
-
} catch (error) {
|
|
912
|
-
// Revert optimistic update on error
|
|
913
|
-
queryClient.invalidateQueries(['connectors']);
|
|
914
|
-
console.error('Action failed:', error);
|
|
915
|
-
}
|
|
916
|
-
};
|
|
917
|
-
|
|
918
|
-
// ... rest of component
|
|
919
|
-
}
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
### Custom Styling with CSS-in-JS
|
|
923
|
-
|
|
924
|
-
```tsx
|
|
925
|
-
import { useConnectors } from '@fastn-ai/react-core';
|
|
926
|
-
import styled from 'styled-components';
|
|
927
|
-
|
|
928
|
-
const ConnectorGrid = styled.div`
|
|
929
|
-
display: grid;
|
|
930
|
-
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
931
|
-
gap: 1.5rem;
|
|
932
|
-
padding: 1rem;
|
|
933
|
-
`;
|
|
934
|
-
|
|
935
|
-
const ConnectorCard = styled.div`
|
|
936
|
-
background: white;
|
|
937
|
-
border: 1px solid #e5e7eb;
|
|
938
|
-
border-radius: 0.5rem;
|
|
939
|
-
padding: 1.5rem;
|
|
940
|
-
transition: all 0.2s ease;
|
|
941
|
-
|
|
942
|
-
&:hover {
|
|
943
|
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
944
|
-
transform: translateY(-2px);
|
|
945
|
-
}
|
|
946
|
-
`;
|
|
947
|
-
|
|
948
|
-
const ActionButton = styled.button<{ variant: 'primary' | 'secondary' | 'danger' }>`
|
|
949
|
-
padding: 0.5rem 1rem;
|
|
950
|
-
border-radius: 0.375rem;
|
|
951
|
-
font-weight: 500;
|
|
952
|
-
transition: all 0.2s ease;
|
|
953
|
-
|
|
954
|
-
${({ variant }) => {
|
|
955
|
-
switch (variant) {
|
|
956
|
-
case 'primary':
|
|
957
|
-
return `
|
|
958
|
-
background: #3b82f6;
|
|
959
|
-
color: white;
|
|
960
|
-
&:hover { background: #2563eb; }
|
|
961
|
-
`;
|
|
962
|
-
case 'secondary':
|
|
963
|
-
return `
|
|
964
|
-
background: #6b7280;
|
|
965
|
-
color: white;
|
|
966
|
-
&:hover { background: #4b5563; }
|
|
967
|
-
`;
|
|
968
|
-
case 'danger':
|
|
969
|
-
return `
|
|
970
|
-
background: #dc2626;
|
|
971
|
-
color: white;
|
|
972
|
-
&:hover { background: #b91c1c; }
|
|
973
|
-
`;
|
|
974
|
-
}
|
|
975
|
-
}}
|
|
976
|
-
`;
|
|
977
|
-
|
|
978
|
-
function StyledConnectorsList() {
|
|
979
|
-
const { data: connectors, isLoading, error } = useConnectors();
|
|
980
|
-
|
|
981
|
-
if (isLoading) return <div>Loading...</div>;
|
|
982
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
983
|
-
|
|
984
|
-
return (
|
|
985
|
-
<ConnectorGrid>
|
|
986
|
-
{connectors?.map(connector => (
|
|
987
|
-
<ConnectorCard key={connector.id}>
|
|
988
|
-
<h3>{connector.name}</h3>
|
|
989
|
-
<p>{connector.description}</p>
|
|
990
|
-
<div>
|
|
991
|
-
{connector.actions.map((action, index) => (
|
|
992
|
-
<ActionButton
|
|
993
|
-
key={index}
|
|
994
|
-
variant={
|
|
995
|
-
action.actionType === 'ACTIVATION' ? 'primary' :
|
|
996
|
-
action.actionType === 'DEACTIVATION' ? 'danger' : 'secondary'
|
|
997
|
-
}
|
|
998
|
-
onClick={() => action.onClick?.()}
|
|
999
|
-
>
|
|
1000
|
-
{action.name}
|
|
1001
|
-
</ActionButton>
|
|
1002
|
-
))}
|
|
1003
|
-
</div>
|
|
1004
|
-
</ConnectorCard>
|
|
1005
|
-
))}
|
|
1006
|
-
</ConnectorGrid>
|
|
1007
|
-
);
|
|
1008
|
-
}
|
|
1009
|
-
```
|
|
1010
|
-
|
|
1011
|
-
## 🎯 Best Practices
|
|
1012
|
-
|
|
1013
|
-
### 1. Error Boundaries
|
|
1014
|
-
|
|
1015
|
-
Wrap your Fastn components with error boundaries to handle unexpected errors gracefully:
|
|
1016
|
-
|
|
1017
|
-
```tsx
|
|
1018
|
-
import { ErrorBoundary } from 'react-error-boundary';
|
|
1019
|
-
|
|
1020
|
-
function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
|
|
1021
|
-
return (
|
|
1022
|
-
<div className="text-center p-6">
|
|
1023
|
-
<h2 className="text-lg font-semibold text-gray-900">Something went wrong</h2>
|
|
1024
|
-
<p className="text-gray-600 mt-2">{error.message}</p>
|
|
1025
|
-
<button
|
|
1026
|
-
onClick={resetErrorBoundary}
|
|
1027
|
-
className="mt-4 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
|
|
1028
|
-
>
|
|
1029
|
-
Try again
|
|
1030
|
-
</button>
|
|
1031
|
-
</div>
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
function App() {
|
|
1036
|
-
return (
|
|
1037
|
-
<FastnProvider config={config}>
|
|
1038
|
-
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
1039
|
-
<YourApp />
|
|
1040
|
-
</ErrorBoundary>
|
|
1041
|
-
</FastnProvider>
|
|
1042
|
-
);
|
|
1043
|
-
}
|
|
1044
|
-
```
|
|
1045
|
-
|
|
1046
|
-
### 2. Loading States
|
|
1047
|
-
|
|
1048
|
-
Always provide meaningful loading states for better user experience:
|
|
1049
|
-
|
|
1050
|
-
```tsx
|
|
1051
|
-
function LoadingSkeleton() {
|
|
1052
|
-
return (
|
|
1053
|
-
<div className="animate-pulse">
|
|
1054
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
1055
|
-
{[...Array(6)].map((_, i) => (
|
|
1056
|
-
<div key={i} className="border rounded-lg p-6">
|
|
1057
|
-
<div className="flex items-center space-x-4">
|
|
1058
|
-
<div className="w-12 h-12 bg-gray-200 rounded-lg"></div>
|
|
1059
|
-
<div className="flex-1">
|
|
1060
|
-
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
|
1061
|
-
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
|
1062
|
-
</div>
|
|
1063
|
-
</div>
|
|
1064
|
-
<div className="mt-4 flex space-x-2">
|
|
1065
|
-
<div className="h-8 bg-gray-200 rounded w-20"></div>
|
|
1066
|
-
<div className="h-8 bg-gray-200 rounded w-20"></div>
|
|
1067
|
-
</div>
|
|
1068
|
-
</div>
|
|
1069
|
-
))}
|
|
1070
|
-
</div>
|
|
1071
|
-
</div>
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
|
-
```
|
|
1075
|
-
|
|
1076
|
-
### 3. Type Safety
|
|
1077
|
-
|
|
1078
|
-
Always use TypeScript interfaces for better type safety:
|
|
1079
|
-
|
|
1080
|
-
```tsx
|
|
1081
|
-
import type {
|
|
1082
|
-
Connector,
|
|
1083
|
-
Configuration,
|
|
1084
|
-
ConfigurationForm,
|
|
1085
|
-
ConnectorField
|
|
1086
|
-
} from '@fastn-ai/react-core';
|
|
1087
|
-
|
|
1088
|
-
interface ConnectorCardProps {
|
|
1089
|
-
connector: Connector;
|
|
1090
|
-
onAction?: (action: ConnectorAction) => Promise<void>;
|
|
1091
|
-
className?: string;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
interface ConfigurationFormProps {
|
|
1095
|
-
form: ConfigurationForm;
|
|
1096
|
-
onSubmit: (data: Record<string, any>) => Promise<void>;
|
|
1097
|
-
onCancel: () => void;
|
|
1098
|
-
loading?: boolean;
|
|
1099
|
-
}
|
|
1100
|
-
```
|
|
1101
|
-
|
|
1102
|
-
### 4. Performance Optimization
|
|
1103
|
-
|
|
1104
|
-
Use React.memo and useMemo for expensive operations:
|
|
1105
|
-
|
|
1106
|
-
```tsx
|
|
1107
|
-
import { memo, useMemo } from 'react';
|
|
1108
|
-
|
|
1109
|
-
const ConnectorCard = memo(({ connector, onAction }: ConnectorCardProps) => {
|
|
1110
|
-
const statusColor = useMemo(() => {
|
|
1111
|
-
return connector.status === 'ACTIVE' ? 'green' : 'gray';
|
|
1112
|
-
}, [connector.status]);
|
|
1113
|
-
|
|
1114
|
-
const actionButtons = useMemo(() => {
|
|
1115
|
-
return connector.actions.map((action, index) => (
|
|
1116
|
-
<button
|
|
1117
|
-
key={index}
|
|
1118
|
-
onClick={() => onAction?.(action)}
|
|
1119
|
-
className={`px-4 py-2 rounded-md ${
|
|
1120
|
-
action.actionType === 'ACTIVATION'
|
|
1121
|
-
? 'bg-blue-600 text-white'
|
|
1122
|
-
: 'bg-gray-600 text-white'
|
|
1123
|
-
}`}
|
|
1124
|
-
>
|
|
1125
|
-
{action.name}
|
|
1126
|
-
</button>
|
|
1127
|
-
));
|
|
1128
|
-
}, [connector.actions, onAction]);
|
|
1129
|
-
|
|
1130
|
-
return (
|
|
1131
|
-
<div className="border rounded-lg p-6">
|
|
1132
|
-
<h3>{connector.name}</h3>
|
|
1133
|
-
<p>{connector.description}</p>
|
|
1134
|
-
<div className="flex space-x-2 mt-4">
|
|
1135
|
-
{actionButtons}
|
|
1136
|
-
</div>
|
|
1137
|
-
</div>
|
|
1138
|
-
);
|
|
1139
|
-
});
|
|
1140
|
-
|
|
1141
|
-
ConnectorCard.displayName = 'ConnectorCard';
|
|
1142
|
-
```
|
|
1143
|
-
|
|
1144
|
-
## 🐛 Troubleshooting
|
|
1145
|
-
|
|
1146
|
-
### Common Issues
|
|
1147
|
-
|
|
1148
|
-
#### 1. Provider Not Found Error
|
|
1149
|
-
|
|
1150
|
-
**Error**: `Initialize Fastn with FastnProvider first`
|
|
1151
|
-
|
|
1152
|
-
**Solution**: Make sure your component is wrapped with `FastnProvider`:
|
|
1153
|
-
|
|
1154
|
-
```tsx
|
|
1155
|
-
// ❌ Wrong - Component outside provider
|
|
1156
|
-
function MyComponent() {
|
|
1157
|
-
const { data } = useConnectors(); // This will throw an error
|
|
1158
|
-
return <div>...</div>;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// ✅ Correct - Component inside provider
|
|
1162
|
-
function App() {
|
|
1163
|
-
return (
|
|
1164
|
-
<FastnProvider config={config}>
|
|
1165
|
-
<MyComponent />
|
|
1166
|
-
</FastnProvider>
|
|
1167
|
-
);
|
|
1168
|
-
}
|
|
1169
|
-
```
|
|
1170
|
-
|
|
1171
|
-
#### 2. Authentication Errors
|
|
1172
|
-
|
|
1173
|
-
**Error**: `401 Unauthorized` or `403 Forbidden`
|
|
1174
|
-
|
|
1175
|
-
**Solution**: Check your configuration:
|
|
1176
|
-
|
|
1177
|
-
```tsx
|
|
1178
|
-
const config = {
|
|
1179
|
-
authToken: 'your-valid-auth-token', // Make sure this is valid
|
|
1180
|
-
tenantId: 'your-tenant-id',
|
|
1181
|
-
spaceId: 'your-space-id',
|
|
1182
|
-
// ... other config
|
|
1183
|
-
};
|
|
1184
|
-
```
|
|
1185
|
-
|
|
1186
|
-
#### 3. Network Errors
|
|
1187
|
-
|
|
1188
|
-
**Error**: `Network Error` or `Failed to fetch`
|
|
1189
|
-
|
|
1190
|
-
**Solution**: Check your base URL and network connectivity:
|
|
1191
|
-
|
|
1192
|
-
```tsx
|
|
1193
|
-
const config = {
|
|
1194
|
-
baseUrl: 'https://api.fastn.ai', // Make sure this is correct
|
|
1195
|
-
// ... other config
|
|
1196
|
-
};
|
|
1197
|
-
```
|
|
1198
|
-
|
|
1199
|
-
#### 4. TypeScript Errors
|
|
1200
|
-
|
|
1201
|
-
**Error**: Type mismatches or missing types
|
|
1202
|
-
|
|
1203
|
-
**Solution**: Import types explicitly:
|
|
1204
|
-
|
|
1205
|
-
```tsx
|
|
1206
|
-
import type {
|
|
1207
|
-
Connector,
|
|
1208
|
-
Configuration,
|
|
1209
|
-
ConnectorAction,
|
|
1210
|
-
ConfigurationAction
|
|
1211
|
-
} from '@fastn-ai/react-core';
|
|
1212
|
-
```
|
|
1213
|
-
|
|
1214
|
-
### Debug Mode
|
|
1215
|
-
|
|
1216
|
-
Enable debug logging to troubleshoot issues:
|
|
1217
|
-
|
|
1218
|
-
```tsx
|
|
1219
|
-
// Add this to your app to see detailed logs
|
|
1220
|
-
if (process.env.NODE_ENV === 'development') {
|
|
1221
|
-
console.log('Fastn Config:', config);
|
|
1222
|
-
}
|
|
1223
|
-
```
|
|
1224
|
-
|
|
1225
|
-
## 📚 Additional Resources
|
|
1226
|
-
|
|
1227
|
-
- [Fastn AI Documentation](https://docs.fastn.ai)
|
|
1228
|
-
- [React Query Documentation](https://tanstack.com/query/latest)
|
|
1229
|
-
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
|
|
1230
|
-
|
|
1231
|
-
## 🤝 Contributing
|
|
1232
|
-
|
|
1233
|
-
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
1234
|
-
|
|
1235
|
-
## 📄 License
|
|
1236
|
-
|
|
1237
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
1238
|
-
|
|
1239
|
-
## 🆘 Support
|
|
1240
|
-
|
|
1241
|
-
If you need help or have questions:
|
|
1
|
+
# @fastn-ai/react-core
|
|
1242
2
|
|
|
1243
|
-
|
|
1244
|
-
- 📖 Documentation: [docs.fastn.ai](https://docs.fastn.ai)
|
|
1245
|
-
- 🐛 Issues: [GitHub Issues](https://github.com/fastn-ai/react-core/issues)
|
|
3
|
+
A professional React library for integrating Fastn AI connectors into your application. This package provides robust hooks and TypeScript types to manage connectors, configurations, and dynamic configuration forms, empowering you to build custom UIs on top of Fastn's data and logic.
|
|
1246
4
|
|
|
1247
5
|
---
|
|
1248
6
|
|
|
1249
|
-
Made with ❤️ by the Fastn team -->
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
# @fastn-ai/react-core
|
|
1253
|
-
|
|
1254
|
-
A React library for integrating Fastn AI connectors into your application. This package provides hooks and types to manage connectors, configurations, and dynamic configuration forms, allowing you to build your own UI on top of Fastn's data and logic.
|
|
1255
|
-
|
|
1256
7
|
## Features
|
|
1257
8
|
|
|
1258
|
-
-
|
|
9
|
+
- List and manage connectors
|
|
1259
10
|
- Handle connector configurations
|
|
1260
11
|
- Render and submit dynamic configuration forms
|
|
1261
12
|
- Powered by React Query (supports custom or existing clients)
|
|
1262
13
|
|
|
14
|
+
---
|
|
15
|
+
|
|
1263
16
|
## Installation
|
|
1264
17
|
|
|
1265
18
|
```bash
|
|
@@ -1268,17 +21,19 @@ npm install @fastn-ai/react-core
|
|
|
1268
21
|
|
|
1269
22
|
### Peer Dependencies
|
|
1270
23
|
|
|
1271
|
-
|
|
24
|
+
Ensure you have React 18+ and React Query installed:
|
|
1272
25
|
|
|
1273
26
|
```bash
|
|
1274
27
|
npm install react react-dom @tanstack/react-query
|
|
1275
28
|
```
|
|
1276
29
|
|
|
1277
|
-
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Getting Started
|
|
1278
33
|
|
|
1279
34
|
### 1. Basic Setup
|
|
1280
35
|
|
|
1281
|
-
Wrap your app with `FastnProvider` and provide your configuration.
|
|
36
|
+
Wrap your app with `FastnProvider` and provide your configuration. By default, this creates its own React Query client.
|
|
1282
37
|
|
|
1283
38
|
```tsx
|
|
1284
39
|
import { FastnProvider } from "@fastn-ai/react-core";
|
|
@@ -1321,11 +76,13 @@ function App() {
|
|
|
1321
76
|
}
|
|
1322
77
|
```
|
|
1323
78
|
|
|
79
|
+
---
|
|
80
|
+
|
|
1324
81
|
## Usage
|
|
1325
82
|
|
|
1326
83
|
### 1. Fetching Connectors
|
|
1327
84
|
|
|
1328
|
-
Use the `useConnectors` hook to
|
|
85
|
+
Use the `useConnectors` hook to retrieve the list of available connectors.
|
|
1329
86
|
|
|
1330
87
|
```tsx
|
|
1331
88
|
import { useConnectors } from "@fastn-ai/react-core";
|
|
@@ -1374,7 +131,7 @@ interface ConnectorAction {
|
|
|
1374
131
|
|
|
1375
132
|
interface ConnectorActionResult {
|
|
1376
133
|
data: unknown;
|
|
1377
|
-
status: "SUCCESS" | "ERROR" | "CANCELLED;
|
|
134
|
+
status: "SUCCESS" | "ERROR" | "CANCELLED";
|
|
1378
135
|
}
|
|
1379
136
|
|
|
1380
137
|
enum ConnectorActionType {
|
|
@@ -1384,6 +141,8 @@ enum ConnectorActionType {
|
|
|
1384
141
|
}
|
|
1385
142
|
```
|
|
1386
143
|
|
|
144
|
+
---
|
|
145
|
+
|
|
1387
146
|
### 2. Fetching Configurations
|
|
1388
147
|
|
|
1389
148
|
Use the `useConfigurations` hook to get connector configurations for a given configuration ID.
|
|
@@ -1440,9 +199,11 @@ type ConfigurationActionType =
|
|
|
1440
199
|
| ConnectorActionType.DELETE;
|
|
1441
200
|
```
|
|
1442
201
|
|
|
202
|
+
---
|
|
203
|
+
|
|
1443
204
|
### 3. Dynamic Configuration Forms
|
|
1444
205
|
|
|
1445
|
-
Use the `useConfigurationForm` hook to fetch a configuration form for a connector or configuration.
|
|
206
|
+
Use the `useConfigurationForm` hook to fetch a configuration form for a connector or configuration. Render the form fields as needed.
|
|
1446
207
|
|
|
1447
208
|
```tsx
|
|
1448
209
|
import { useConfigurationForm } from "@fastn-ai/react-core";
|
|
@@ -1456,8 +217,7 @@ function ConfigurationForm({ configurationId }: { configurationId: string }) {
|
|
|
1456
217
|
return (
|
|
1457
218
|
<form onSubmit={/* your submit handler */}>
|
|
1458
219
|
{configurationForm.fields.map((field) => (
|
|
1459
|
-
// Render
|
|
1460
|
-
<div key={field.name}>{field.label}</div>
|
|
220
|
+
// Render field based on type (see below for select and Google Drive picker fields)
|
|
1461
221
|
))}
|
|
1462
222
|
<button type="submit">Submit</button>
|
|
1463
223
|
</form>
|
|
@@ -1486,13 +246,13 @@ interface ConnectorField {
|
|
|
1486
246
|
}
|
|
1487
247
|
```
|
|
1488
248
|
|
|
1489
|
-
|
|
249
|
+
---
|
|
1490
250
|
|
|
1491
|
-
|
|
251
|
+
### 4. Advanced Field Handling: Select & Google Drive File Picker
|
|
1492
252
|
|
|
1493
253
|
#### Select Fields (Single & Multi-Select)
|
|
1494
254
|
|
|
1495
|
-
For fields of type `select` or `multi-select`, use the `useFieldOptions` hook to fetch options, handle search, pagination, and errors. This
|
|
255
|
+
For fields of type `select` or `multi-select`, use the `useFieldOptions` hook to fetch options, handle search, pagination, and errors. This abstracts away the complexity of loading options from remote sources.
|
|
1496
256
|
|
|
1497
257
|
**Example: Generic SelectField Component**
|
|
1498
258
|
|
|
@@ -1500,7 +260,6 @@ For fields of type `select` or `multi-select`, use the `useFieldOptions` hook to
|
|
|
1500
260
|
import { useFieldOptions } from "@fastn-ai/react-core";
|
|
1501
261
|
|
|
1502
262
|
function SelectField({ field, value, onChange, isMulti = false }) {
|
|
1503
|
-
// useFieldOptions provides all logic for options, loading, errors, search, pagination, etc.
|
|
1504
263
|
const {
|
|
1505
264
|
options,
|
|
1506
265
|
loading,
|
|
@@ -1513,17 +272,14 @@ function SelectField({ field, value, onChange, isMulti = false }) {
|
|
|
1513
272
|
totalLoadedOptions,
|
|
1514
273
|
} = useFieldOptions(field);
|
|
1515
274
|
|
|
1516
|
-
// Handle search input
|
|
1517
275
|
function handleInputChange(e) {
|
|
1518
276
|
search(e.target.value);
|
|
1519
277
|
}
|
|
1520
278
|
|
|
1521
|
-
// Handle loading more options (pagination)
|
|
1522
279
|
function handleLoadMore() {
|
|
1523
280
|
if (hasNext && !loadingMore) loadMore();
|
|
1524
281
|
}
|
|
1525
282
|
|
|
1526
|
-
// Handle refresh on error
|
|
1527
283
|
function handleRefresh() {
|
|
1528
284
|
refresh();
|
|
1529
285
|
}
|
|
@@ -1572,12 +328,8 @@ function SelectField({ field, value, onChange, isMulti = false }) {
|
|
|
1572
328
|
}
|
|
1573
329
|
```
|
|
1574
330
|
|
|
1575
|
-
**Key Points:**
|
|
1576
|
-
- `useFieldOptions(field)` returns all logic for options, loading, errors, search, pagination, and refresh.
|
|
1577
|
-
- Use `search` for filtering options, `loadMore` for pagination, and `refresh` to retry on error.
|
|
1578
|
-
- The component above is generic and can be styled or replaced with any UI library (e.g., react-select).
|
|
1579
|
-
|
|
1580
331
|
**useFieldOptions Return Type:**
|
|
332
|
+
|
|
1581
333
|
```ts
|
|
1582
334
|
interface UseFieldOptionsReturn {
|
|
1583
335
|
options: Array<{ label: string; value: string }>;
|
|
@@ -1594,21 +346,16 @@ interface UseFieldOptionsReturn {
|
|
|
1594
346
|
|
|
1595
347
|
#### Google Drive File Picker Field
|
|
1596
348
|
|
|
1597
|
-
For fields
|
|
349
|
+
For fields requiring Google Drive file selection, the field type will be `google-files-picker-select` or similar. These fields provide an `optionsSource` with a method to open the Google Files Picker dialog.
|
|
1598
350
|
|
|
1599
351
|
**Example: GoogleFilesPickerField Component**
|
|
1600
352
|
|
|
1601
353
|
```tsx
|
|
1602
354
|
function GoogleFilesPickerField({ field, value, onChange, isMulti = false }) {
|
|
1603
|
-
// This field may also use useFieldOptions for listing previously picked files
|
|
1604
|
-
const { options, loading, error, refresh } = useFieldOptions(field);
|
|
1605
|
-
|
|
1606
|
-
// Handler to open Google Drive picker
|
|
1607
355
|
async function handlePickFiles() {
|
|
1608
356
|
if (field.optionsSource?.openGoogleFilesPicker) {
|
|
1609
357
|
await field.optionsSource.openGoogleFilesPicker({
|
|
1610
358
|
onComplete: async (files) => {
|
|
1611
|
-
// files is an array of selected file objects
|
|
1612
359
|
if (isMulti) {
|
|
1613
360
|
onChange(files);
|
|
1614
361
|
} else {
|
|
@@ -1616,7 +363,6 @@ function GoogleFilesPickerField({ field, value, onChange, isMulti = false }) {
|
|
|
1616
363
|
}
|
|
1617
364
|
},
|
|
1618
365
|
onError: async (pickerError) => {
|
|
1619
|
-
// Handle picker error (optional)
|
|
1620
366
|
alert('Google Files Picker error: ' + pickerError);
|
|
1621
367
|
},
|
|
1622
368
|
});
|
|
@@ -1626,13 +372,7 @@ function GoogleFilesPickerField({ field, value, onChange, isMulti = false }) {
|
|
|
1626
372
|
return (
|
|
1627
373
|
<div>
|
|
1628
374
|
<label>{field.label}{field.required && ' *'}</label>
|
|
1629
|
-
|
|
1630
|
-
<div>
|
|
1631
|
-
<span style={{ color: 'red' }}>Error loading files</span>
|
|
1632
|
-
<button type="button" onClick={refresh}>Retry</button>
|
|
1633
|
-
</div>
|
|
1634
|
-
)}
|
|
1635
|
-
<button type="button" onClick={handlePickFiles} disabled={loading}>
|
|
375
|
+
<button type="button" onClick={handlePickFiles}>
|
|
1636
376
|
Pick from Google Drive
|
|
1637
377
|
</button>
|
|
1638
378
|
{value && (
|
|
@@ -1640,7 +380,7 @@ function GoogleFilesPickerField({ field, value, onChange, isMulti = false }) {
|
|
|
1640
380
|
<strong>Selected file{isMulti ? 's' : ''}:</strong>
|
|
1641
381
|
<ul>
|
|
1642
382
|
{(isMulti ? value : [value]).map((file, idx) => (
|
|
1643
|
-
<li key={file.
|
|
383
|
+
<li key={file.value || idx}>{file.label || file.value}</li>
|
|
1644
384
|
))}
|
|
1645
385
|
</ul>
|
|
1646
386
|
</div>
|
|
@@ -1651,13 +391,8 @@ function GoogleFilesPickerField({ field, value, onChange, isMulti = false }) {
|
|
|
1651
391
|
}
|
|
1652
392
|
```
|
|
1653
393
|
|
|
1654
|
-
**Key Points:**
|
|
1655
|
-
- The `openGoogleFilesPicker` method is provided on the field's `optionsSource` property.
|
|
1656
|
-
- The `onComplete` callback receives the selected files (array of file objects).
|
|
1657
|
-
- You can display the selected files as you wish.
|
|
1658
|
-
- This field can be used in both single and multi-select modes.
|
|
1659
|
-
|
|
1660
394
|
**GoogleFilesPicker Return Type:**
|
|
395
|
+
|
|
1661
396
|
```ts
|
|
1662
397
|
interface UseGoogleFilesPickerReturn {
|
|
1663
398
|
options: Array<{ label: string; value: string }>;
|
|
@@ -1673,19 +408,21 @@ interface UseGoogleFilesPickerReturn {
|
|
|
1673
408
|
}
|
|
1674
409
|
```
|
|
1675
410
|
|
|
1676
|
-
**
|
|
1677
|
-
-
|
|
1678
|
-
-
|
|
1679
|
-
-
|
|
411
|
+
**Integration Tips:**
|
|
412
|
+
- Use these fields as controlled components in your form library (Formik, React Hook Form, etc.).
|
|
413
|
+
- Manage `value` and `onChange` via your form state.
|
|
414
|
+
- Combine with other input types to build a complete dynamic configuration form.
|
|
1680
415
|
|
|
1681
416
|
---
|
|
1682
417
|
|
|
1683
418
|
## Error Handling & Troubleshooting
|
|
1684
419
|
|
|
1685
|
-
- **Provider Not Found
|
|
1686
|
-
- **Authentication Errors
|
|
1687
|
-
- **Network Errors
|
|
1688
|
-
- **TypeScript Errors
|
|
420
|
+
- **Provider Not Found:** Ensure your components are wrapped in `FastnProvider`.
|
|
421
|
+
- **Authentication Errors:** Check your `authToken`, `tenantId`, and `spaceId`.
|
|
422
|
+
- **Network Errors:** Verify your network and API base URL.
|
|
423
|
+
- **TypeScript Errors:** Import types from the package as needed.
|
|
424
|
+
|
|
425
|
+
---
|
|
1689
426
|
|
|
1690
427
|
## TypeScript Support
|
|
1691
428
|
|
|
@@ -1695,16 +432,19 @@ All hooks and data structures are fully typed. You can import types directly:
|
|
|
1695
432
|
import type { Connector, Configuration, ConnectorAction, ConfigurationAction } from '@fastn-ai/react-core';
|
|
1696
433
|
```
|
|
1697
434
|
|
|
435
|
+
---
|
|
436
|
+
|
|
1698
437
|
## License
|
|
1699
438
|
|
|
1700
439
|
MIT License. See the [LICENSE](LICENSE) file for details.
|
|
1701
440
|
|
|
441
|
+
---
|
|
442
|
+
|
|
1702
443
|
## Support
|
|
1703
444
|
|
|
1704
445
|
- Email: support@fastn.ai
|
|
1705
|
-
- Documentation: https://docs.fastn.ai
|
|
1706
|
-
- Issues: https://github.com/fastn-ai/react-core/issues
|
|
446
|
+
- Documentation: [https://docs.fastn.ai](https://docs.fastn.ai)
|
|
1707
447
|
|
|
1708
448
|
---
|
|
1709
449
|
|
|
1710
|
-
This package provides the data and logic for Fastn AI connectors. You are free to build your own UI and integrate these hooks and types as needed for your application.
|
|
450
|
+
This package provides the data and logic for Fastn AI connectors. You are free to build your own UI and integrate these hooks and types as needed for your application.
|