gufi-cli 0.1.49 → 0.1.51
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/dist/commands/docs.js +1 -5
- package/dist/lib/docs-resolver.d.ts +8 -0
- package/dist/lib/docs-resolver.js +27 -0
- package/dist/lib/security.js +5 -0
- package/dist/mcp.js +56 -43
- package/docs/dev-guide/1-01-architecture.md +358 -0
- package/docs/dev-guide/1-02-multi-tenant.md +415 -0
- package/docs/dev-guide/1-03-column-types.md +594 -0
- package/docs/dev-guide/1-04-json-config.md +442 -0
- package/docs/dev-guide/1-05-authentication.md +427 -0
- package/docs/dev-guide/2-01-api-reference.md +564 -0
- package/docs/dev-guide/2-02-automations.md +508 -0
- package/docs/dev-guide/2-03-gufi-cli.md +568 -0
- package/docs/dev-guide/2-04-realtime.md +401 -0
- package/docs/dev-guide/2-05-permissions.md +497 -0
- package/docs/dev-guide/2-06-integrations-overview.md +104 -0
- package/docs/dev-guide/2-07-stripe.md +173 -0
- package/docs/dev-guide/2-08-nayax.md +297 -0
- package/docs/dev-guide/2-09-ourvend.md +226 -0
- package/docs/dev-guide/2-10-tns.md +177 -0
- package/docs/dev-guide/2-11-custom-http.md +268 -0
- package/docs/dev-guide/3-01-custom-views.md +555 -0
- package/docs/dev-guide/3-02-webhooks-api.md +446 -0
- package/docs/mcp/00-overview.md +329 -0
- package/docs/mcp/01-architecture.md +226 -0
- package/docs/mcp/02-modules.md +285 -0
- package/docs/mcp/03-fields.md +357 -0
- package/docs/mcp/04-views.md +613 -0
- package/docs/mcp/05-automations.md +461 -0
- package/docs/mcp/06-api.md +531 -0
- package/docs/mcp/07-packages.md +246 -0
- package/docs/mcp/08-common-errors.md +284 -0
- package/docs/mcp/09-examples.md +453 -0
- package/docs/mcp/README.md +71 -0
- package/docs/mcp/tool-descriptions.json +64 -0
- package/package.json +3 -2
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: custom-views
|
|
3
|
+
title: "Custom Views Development"
|
|
4
|
+
description: "Build custom views for the Marketplace"
|
|
5
|
+
icon: Store
|
|
6
|
+
category: dev
|
|
7
|
+
part: 3
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Custom Views Development
|
|
11
|
+
|
|
12
|
+
Build custom views for the Marketplace
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Custom views are React components that extend Gufi's functionality. They can:
|
|
17
|
+
|
|
18
|
+
- Display data in unique ways (dashboards, charts, maps)
|
|
19
|
+
- Provide specialized workflows
|
|
20
|
+
- Integrate with external services
|
|
21
|
+
- Offer industry-specific features
|
|
22
|
+
|
|
23
|
+
## View Structure
|
|
24
|
+
|
|
25
|
+
### Standard Directory Layout
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
my_view/
|
|
29
|
+
├── index.tsx # Main export + featureConfig
|
|
30
|
+
├── types.ts # TypeScript types
|
|
31
|
+
├── core/
|
|
32
|
+
│ ├── dataSources.ts # Data sources (REQUIRED)
|
|
33
|
+
│ ├── automations.ts # Automations declaration (if using automations)
|
|
34
|
+
│ └── index.tsx # featureConfig
|
|
35
|
+
├── metadata/
|
|
36
|
+
│ ├── seedData.ts # Demo data for preview
|
|
37
|
+
│ ├── inputs.ts # User-configurable fields
|
|
38
|
+
│ └── migrations.ts # SQL migrations
|
|
39
|
+
├── components/ # UI components
|
|
40
|
+
│ ├── Header.tsx
|
|
41
|
+
│ ├── Dashboard.tsx
|
|
42
|
+
│ └── ...
|
|
43
|
+
└── plugin/ # Optional: ViewPlugin for native
|
|
44
|
+
└── MyViewPlugin.tsx
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Main Entry (index.tsx)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import React from 'react';
|
|
51
|
+
import type { FullGufiContext } from '@/sdk';
|
|
52
|
+
import { featureConfig } from './core/dataProvider';
|
|
53
|
+
import { MyDashboard } from './components/Dashboard';
|
|
54
|
+
|
|
55
|
+
interface Props {
|
|
56
|
+
id: string;
|
|
57
|
+
gufi: FullGufiContext;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default function MyView({ id, gufi }: Props) {
|
|
61
|
+
return <MyDashboard id={id} gufi={gufi} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { featureConfig };
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Feature Configuration
|
|
68
|
+
|
|
69
|
+
### Basic Config
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// core/dataProvider.ts
|
|
73
|
+
import type { FeatureConfig } from '@/sdk';
|
|
74
|
+
|
|
75
|
+
export const featureConfig: FeatureConfig = {
|
|
76
|
+
id: 'my-analytics-view',
|
|
77
|
+
name: 'My Analytics',
|
|
78
|
+
description: 'Analytics dashboard for sales data',
|
|
79
|
+
version: '1.0.0',
|
|
80
|
+
author: 'Your Name',
|
|
81
|
+
icon: 'BarChart3',
|
|
82
|
+
|
|
83
|
+
// Data requirements
|
|
84
|
+
dataSources: [
|
|
85
|
+
{
|
|
86
|
+
key: 'ordersTable',
|
|
87
|
+
name: 'Orders',
|
|
88
|
+
description: 'Table containing orders',
|
|
89
|
+
required: true
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: 'productsTable',
|
|
93
|
+
name: 'Products',
|
|
94
|
+
description: 'Products catalog',
|
|
95
|
+
required: false
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
|
|
99
|
+
// User inputs (shown in settings)
|
|
100
|
+
inputs: [
|
|
101
|
+
{
|
|
102
|
+
key: 'defaultPeriod',
|
|
103
|
+
type: 'select',
|
|
104
|
+
label: 'Default Period',
|
|
105
|
+
options: ['week', 'month', 'quarter', 'year'],
|
|
106
|
+
default: 'month'
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Data Sources
|
|
113
|
+
|
|
114
|
+
Define what tables your view needs:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
dataSources: [
|
|
118
|
+
{
|
|
119
|
+
key: 'ordersTable', // Reference key
|
|
120
|
+
name: 'Orders Table', // Display name
|
|
121
|
+
description: 'Sales orders', // Help text
|
|
122
|
+
required: true, // Must be mapped
|
|
123
|
+
columns: [ // Optional: required columns
|
|
124
|
+
{ name: 'total', type: 'currency' },
|
|
125
|
+
{ name: 'status', type: 'select' }
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### User Inputs
|
|
132
|
+
|
|
133
|
+
Configuration options shown to users:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
inputs: [
|
|
137
|
+
{
|
|
138
|
+
key: 'theme',
|
|
139
|
+
type: 'select',
|
|
140
|
+
label: 'Color Theme',
|
|
141
|
+
options: ['purple', 'blue', 'green'],
|
|
142
|
+
default: 'purple'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'showTrends',
|
|
146
|
+
type: 'checkbox',
|
|
147
|
+
label: 'Show Trend Lines',
|
|
148
|
+
default: true
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: 'apiEndpoint',
|
|
152
|
+
type: 'text',
|
|
153
|
+
label: 'External API URL',
|
|
154
|
+
placeholder: 'https://api.example.com'
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## The gufi Context
|
|
160
|
+
|
|
161
|
+
### What's Available
|
|
162
|
+
|
|
163
|
+
Every view receives the `gufi` context:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
interface FullGufiContext {
|
|
167
|
+
// Data Provider
|
|
168
|
+
dataProvider: {
|
|
169
|
+
getList: (params) => Promise<{ data, total }>;
|
|
170
|
+
getOne: (params) => Promise<{ data }>;
|
|
171
|
+
create: (params) => Promise<{ data }>;
|
|
172
|
+
update: (params) => Promise<{ data }>;
|
|
173
|
+
delete: (params) => Promise<void>;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Automations (con acceso a api.stripe.*, api.nayax.*, etc.)
|
|
177
|
+
automation: (name: string, input?: object) => Promise<any>;
|
|
178
|
+
|
|
179
|
+
// WebSocket
|
|
180
|
+
websocket: {
|
|
181
|
+
subscribeToTable: (tableId, callback) => () => void;
|
|
182
|
+
broadcastToView: (event, data) => void;
|
|
183
|
+
isConnected: () => boolean;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Context Info
|
|
187
|
+
context: {
|
|
188
|
+
schema: CompanySchema;
|
|
189
|
+
user: { id, name, email };
|
|
190
|
+
activeCompany: { id, name };
|
|
191
|
+
lang: 'es' | 'en';
|
|
192
|
+
viewSpec: Record<string, string>; // Table mappings
|
|
193
|
+
seedData: SeedDataConfig;
|
|
194
|
+
isPreview: boolean;
|
|
195
|
+
isDev: boolean;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Utilities
|
|
199
|
+
utils: {
|
|
200
|
+
// Translations
|
|
201
|
+
tUI: (key) => string;
|
|
202
|
+
getLang: () => 'es' | 'en';
|
|
203
|
+
|
|
204
|
+
// Notifications
|
|
205
|
+
toastSuccess: (msg) => void;
|
|
206
|
+
toastError: (msg) => void;
|
|
207
|
+
toastInfo: (msg) => void;
|
|
208
|
+
|
|
209
|
+
// Formatting
|
|
210
|
+
formatNumber: (n) => string;
|
|
211
|
+
formatCurrency: (n) => string;
|
|
212
|
+
formatDate: (d) => string;
|
|
213
|
+
formatDateTime: (d) => string;
|
|
214
|
+
|
|
215
|
+
// Data helpers
|
|
216
|
+
groupBy: (arr, key) => Record;
|
|
217
|
+
sumBy: (arr, key) => number;
|
|
218
|
+
sortBy: (arr, key, order) => array;
|
|
219
|
+
|
|
220
|
+
// Other
|
|
221
|
+
resolveTableName: (schema, ref) => string;
|
|
222
|
+
navigate: (path) => void;
|
|
223
|
+
cn: (...classes) => string;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Device Info
|
|
227
|
+
device: {
|
|
228
|
+
isHandheld: boolean;
|
|
229
|
+
isDesktop: boolean;
|
|
230
|
+
isMobile: boolean;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Using the Context
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
export default function MyView({ id, gufi }: Props) {
|
|
239
|
+
const { dataProvider, utils, context, device } = gufi;
|
|
240
|
+
const { tUI, toastSuccess, formatCurrency } = utils;
|
|
241
|
+
|
|
242
|
+
// Get table ID from viewSpec mapping
|
|
243
|
+
const ordersTable = context.viewSpec?.ordersTable;
|
|
244
|
+
|
|
245
|
+
// Load data
|
|
246
|
+
const { data: orders } = useViewData({
|
|
247
|
+
gufi,
|
|
248
|
+
tableKey: 'ordersTable'
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Responsive layout
|
|
252
|
+
if (device.isHandheld) {
|
|
253
|
+
return <MobileLayout orders={orders} />;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return <DesktopLayout orders={orders} />;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Seed Data (Preview Mode)
|
|
261
|
+
|
|
262
|
+
### Defining Demo Data
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// metadata/seedData.ts
|
|
266
|
+
import type { SeedDataConfig } from '@/sdk';
|
|
267
|
+
|
|
268
|
+
export const seedData: SeedDataConfig = {
|
|
269
|
+
ordersTable: [
|
|
270
|
+
{
|
|
271
|
+
id: '@ref:ordersTable.0',
|
|
272
|
+
customer_name: 'Acme Corp',
|
|
273
|
+
total: { currency: 'EUR', amount: 1500 },
|
|
274
|
+
status: 'completed',
|
|
275
|
+
created_at: '@today'
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: '@ref:ordersTable.1',
|
|
279
|
+
customer_name: 'Beta Inc',
|
|
280
|
+
total: { currency: 'EUR', amount: 800 },
|
|
281
|
+
status: 'pending',
|
|
282
|
+
created_at: '@today'
|
|
283
|
+
}
|
|
284
|
+
],
|
|
285
|
+
productsTable: [
|
|
286
|
+
{
|
|
287
|
+
id: '@ref:productsTable.0',
|
|
288
|
+
name: 'Widget Pro',
|
|
289
|
+
price: { currency: 'EUR', amount: 99.99 },
|
|
290
|
+
stock: 150
|
|
291
|
+
}
|
|
292
|
+
]
|
|
293
|
+
};
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Special Tokens
|
|
297
|
+
|
|
298
|
+
| Token | Value |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `@ref:tableName.index` | Auto-generated ID |
|
|
301
|
+
| `@today` | Current date |
|
|
302
|
+
| `@currentUser` | Current user ID |
|
|
303
|
+
| `@now` | Current timestamp |
|
|
304
|
+
|
|
305
|
+
### Using Seed Data
|
|
306
|
+
|
|
307
|
+
The SDK automatically uses seed data in preview mode:
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
const { data, loading, usingSeedData } = useViewData({
|
|
311
|
+
gufi,
|
|
312
|
+
tableKey: 'ordersTable'
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// usingSeedData is true when in preview
|
|
316
|
+
if (usingSeedData) {
|
|
317
|
+
console.log('Showing demo data');
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Automations
|
|
322
|
+
|
|
323
|
+
### Declaring Automations
|
|
324
|
+
|
|
325
|
+
If your view needs to call automations (for Stripe payments, email sending, etc.), you must declare them in `core/automations.ts`:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// core/automations.ts
|
|
329
|
+
export const automations = [
|
|
330
|
+
{ name: 'stripe_checkout', description: 'Create Stripe payment session' },
|
|
331
|
+
{ name: 'send_confirmation', description: 'Send confirmation email' },
|
|
332
|
+
] as const;
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Important:**
|
|
336
|
+
- Automations must be declared BEFORE they can be called from the view
|
|
337
|
+
- On `gufi view:push`, declared automations are registered in `__automation_meta__` with `trigger_event='CUSTOM'`
|
|
338
|
+
- Only declared automations can be executed via `gufi.automation()`
|
|
339
|
+
|
|
340
|
+
### Calling Automations
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
const handlePayment = async () => {
|
|
344
|
+
try {
|
|
345
|
+
// Only works if 'stripe_checkout' is declared in core/automations.ts
|
|
346
|
+
const result = await gufi.automation('stripe_checkout', {
|
|
347
|
+
amount: 1999,
|
|
348
|
+
customer_email: 'user@example.com',
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (result.url) {
|
|
352
|
+
window.location.href = result.url;
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
gufi.utils.toastError('Payment failed');
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Workflow
|
|
361
|
+
|
|
362
|
+
1. **Create script:** `gufi automation:create stripe_checkout script.js -c 116`
|
|
363
|
+
2. **Declare in view:** Add to `core/automations.ts`
|
|
364
|
+
3. **Push view:** `gufi view:push` → Registers in `__automation_meta__`
|
|
365
|
+
4. **Use in view:** `gufi.automation('stripe_checkout', input)`
|
|
366
|
+
|
|
367
|
+
## Migrations
|
|
368
|
+
|
|
369
|
+
### Version Updates
|
|
370
|
+
|
|
371
|
+
When your view schema changes:
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// metadata/migrations.ts
|
|
375
|
+
export const migrations = [
|
|
376
|
+
{
|
|
377
|
+
version: '1.1.0',
|
|
378
|
+
description: 'Add status column',
|
|
379
|
+
up: `
|
|
380
|
+
ALTER TABLE {tableName}
|
|
381
|
+
ADD COLUMN IF NOT EXISTS status VARCHAR(50) DEFAULT 'active';
|
|
382
|
+
`,
|
|
383
|
+
down: `
|
|
384
|
+
ALTER TABLE {tableName}
|
|
385
|
+
DROP COLUMN IF EXISTS status;
|
|
386
|
+
`
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
version: '1.2.0',
|
|
390
|
+
description: 'Add priority field',
|
|
391
|
+
up: `
|
|
392
|
+
ALTER TABLE {tableName}
|
|
393
|
+
ADD COLUMN IF NOT EXISTS priority INTEGER DEFAULT 0;
|
|
394
|
+
`,
|
|
395
|
+
down: `
|
|
396
|
+
ALTER TABLE {tableName}
|
|
397
|
+
DROP COLUMN IF EXISTS priority;
|
|
398
|
+
`
|
|
399
|
+
}
|
|
400
|
+
];
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Placeholders
|
|
404
|
+
|
|
405
|
+
| Placeholder | Value |
|
|
406
|
+
|---|---|
|
|
407
|
+
| `{tableName}` | Physical table name |
|
|
408
|
+
| `{entityId}` | Entity ID |
|
|
409
|
+
| `{companyId}` | Company ID |
|
|
410
|
+
| `{schemaName}` | Company schema |
|
|
411
|
+
|
|
412
|
+
## Best Practices
|
|
413
|
+
|
|
414
|
+
### Data Loading
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// Good: Use SDK hooks
|
|
418
|
+
const { data, loading, error, refetch } = useViewData({
|
|
419
|
+
gufi,
|
|
420
|
+
tableKey: 'ordersTable',
|
|
421
|
+
filters: { status: 'active' }
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Bad: Direct store access
|
|
425
|
+
import { useCompanySchemaStore } from '@/stores'; // Don't do this
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Error Handling
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
if (error) {
|
|
432
|
+
return (
|
|
433
|
+
<div className="p-4 text-red-600">
|
|
434
|
+
<AlertCircle className="w-5 h-5" />
|
|
435
|
+
<span>{utils.tUI('common.error')}: {error.message}</span>
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Loading States
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
if (loading) {
|
|
445
|
+
return (
|
|
446
|
+
<div className="animate-pulse">
|
|
447
|
+
<div className="h-8 bg-zinc-200 rounded" />
|
|
448
|
+
<div className="h-32 bg-zinc-100 rounded mt-4" />
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Responsive Design
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
const { device } = gufi;
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
<div className={device.isHandheld ? 'p-2' : 'p-6'}>
|
|
461
|
+
{device.isDesktop && <Sidebar />}
|
|
462
|
+
<MainContent />
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Development Workflow
|
|
468
|
+
|
|
469
|
+
### Local Development
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
# 1. Pull view for editing
|
|
473
|
+
gufi pull 55
|
|
474
|
+
cd view_55/
|
|
475
|
+
|
|
476
|
+
# 2. Start watch mode
|
|
477
|
+
gufi watch
|
|
478
|
+
|
|
479
|
+
# 3. Make changes - auto syncs
|
|
480
|
+
|
|
481
|
+
# 4. View logs
|
|
482
|
+
gufi logs
|
|
483
|
+
|
|
484
|
+
# 5. Test in LivePreview
|
|
485
|
+
# Open view in Gufi web interface
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Publishing
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
# 1. Add to package
|
|
492
|
+
gufi package:add-view 14 55
|
|
493
|
+
|
|
494
|
+
# 2. Check changes
|
|
495
|
+
gufi package:check 14
|
|
496
|
+
|
|
497
|
+
# 3. Sync version
|
|
498
|
+
gufi package:sync 14
|
|
499
|
+
|
|
500
|
+
# 4. Publish
|
|
501
|
+
gufi package:publish 14
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## Common Patterns
|
|
505
|
+
|
|
506
|
+
### Dashboard with KPIs
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
import { ViewLayout, ViewKPIGrid, ViewKPI, ViewCard } from '@/shared/views';
|
|
510
|
+
|
|
511
|
+
export function Dashboard({ gufi }: Props) {
|
|
512
|
+
const { data } = useViewData({ gufi, tableKey: 'ordersTable' });
|
|
513
|
+
|
|
514
|
+
const totalRevenue = gufi.utils.sumBy(data, 'total.amount');
|
|
515
|
+
const orderCount = data.length;
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<ViewLayout>
|
|
519
|
+
<ViewKPIGrid>
|
|
520
|
+
<ViewKPI
|
|
521
|
+
title="Total Revenue"
|
|
522
|
+
value={gufi.utils.formatCurrency(totalRevenue)}
|
|
523
|
+
trend="+12%"
|
|
524
|
+
trendUp
|
|
525
|
+
/>
|
|
526
|
+
<ViewKPI
|
|
527
|
+
title="Orders"
|
|
528
|
+
value={orderCount}
|
|
529
|
+
/>
|
|
530
|
+
</ViewKPIGrid>
|
|
531
|
+
|
|
532
|
+
<ViewCard title="Recent Orders">
|
|
533
|
+
<OrdersTable orders={data} />
|
|
534
|
+
</ViewCard>
|
|
535
|
+
</ViewLayout>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Real-Time Updates
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
useEffect(() => {
|
|
544
|
+
const tableId = context.viewSpec?.ordersTable;
|
|
545
|
+
if (!tableId) return;
|
|
546
|
+
|
|
547
|
+
const unsubscribe = gufi.websocket.subscribeToTable(tableId, (event) => {
|
|
548
|
+
if (event.type === 'INSERT' || event.type === 'UPDATE') {
|
|
549
|
+
refetch();
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
return () => unsubscribe();
|
|
554
|
+
}, [context.viewSpec?.ordersTable]);
|
|
555
|
+
```
|