@svgrid/enterprise 2.0.3 → 2.2.0
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 +18 -6
- package/dist/cdn/svgrid-enterprise.svelte-external.js +14025 -6836
- package/dist/node/studio.js +7889 -2460
- package/package.json +9 -4
- package/src/SvGridMasterDetail.svelte +24 -3
- package/src/SvGridScheduler.svelte +4410 -0
- package/src/SvPivotDesigner.svelte +1990 -1045
- package/src/SvSchemaChart.svelte +10 -9
- package/src/ai.test.ts +522 -522
- package/src/ai.ts +202 -2
- package/src/index.ts +409 -384
- package/src/install.ts +10 -0
- package/src/pivot-chart.test.ts +86 -0
- package/src/pivot-chart.ts +112 -0
- package/src/scheduler.ts +37 -0
- package/src/scheduling.test.ts +194 -0
- package/src/scheduling.ts +293 -0
- package/src/sources/filters.ts +6 -0
- package/src/studio/HANDLERS-DESIGN.md +142 -0
- package/src/studio/cli.ts +7 -2
- package/src/studio/emit-project.test.ts +1447 -13
- package/src/studio/emit-project.ts +3995 -1273
- package/src/studio/emit-schema.ts +146 -29
- package/src/studio/index.ts +320 -195
- package/src/studio/project.test.ts +370 -0
- package/src/studio/project.ts +1146 -26
- package/src/studio/sample-data.ts +4 -1
- package/src/studio/samples/ats.ts +2 -2
- package/src/studio/samples/clinic.ts +4 -2
- package/src/studio/samples/crm.ts +16 -8
- package/src/studio/samples/events.ts +4 -2
- package/src/studio/samples/fleet.ts +4 -2
- package/src/studio/samples/gym.ts +4 -2
- package/src/studio/samples/hr.ts +3 -1
- package/src/studio/samples/live-data.ts +308 -308
- package/src/studio/samples/projects.ts +2 -2
- package/src/studio/samples/restaurant.ts +4 -2
- package/src/studio/samples/samples.test.ts +13 -5
- package/src/studio/samples/shared.ts +346 -305
- package/src/studio/samples/support.ts +3 -1
- package/src/studio/scaffold.test.ts +15 -1
- package/src/studio/scaffold.ts +16 -0
- package/src/studio/themes.ts +7 -0
- package/src/studio/ui-components.ts +472 -0
- package/src/sveltekit/transport.test.ts +26 -0
- package/src/sveltekit/transport.ts +50 -5
- package/dist/designer/assets/index-Dp44bTid.js +0 -939
- package/dist/designer/assets/index-RJp6x8tw.css +0 -1
- package/dist/designer/assets/jszip.min-CjMo-QGg.js +0 -2
- package/dist/designer/index.html +0 -13
|
@@ -1,308 +1,308 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* "Start from live data" gallery: starter `StudioProject`s wired to a REAL data
|
|
3
|
-
* source instead of an in-memory seed - so one click gives you an app that talks
|
|
4
|
-
* to actual Postgres or a live API, then generates a runnable project bound to
|
|
5
|
-
* the same source.
|
|
6
|
-
*
|
|
7
|
-
* These are deliberately separate from `sampleApps` (which are polished,
|
|
8
|
-
* memory-backed, enterprise-layout showcases). A live-data starter's value is
|
|
9
|
-
* the *binding*: embedded Postgres (PGlite), a hosted Supabase project, or a
|
|
10
|
-
* public REST API.
|
|
11
|
-
*
|
|
12
|
-
* Pure + node-safe: plain data built from the same ops the designer uses, so
|
|
13
|
-
* each round-trips through parse/serialize and generates like any project.
|
|
14
|
-
*/
|
|
15
|
-
import type { EntitySchema } from '../../schema.js'
|
|
16
|
-
import { listScreen, dashScreen, detailScreen, type SampleApp } from './shared.js'
|
|
17
|
-
import { sanitizeProject, type EntityDataSource, type Screen, type StudioProject } from '../project.js'
|
|
18
|
-
|
|
19
|
-
type Row = Record<string, unknown>
|
|
20
|
-
|
|
21
|
-
// ---- Northwind schemas (shared by the PGlite + Supabase starters) ----------
|
|
22
|
-
|
|
23
|
-
const products: EntitySchema = {
|
|
24
|
-
name: 'products',
|
|
25
|
-
label: 'Product',
|
|
26
|
-
idField: 'id',
|
|
27
|
-
fields: [
|
|
28
|
-
{ field: 'id', type: 'number', primaryKey: true, readonly: true },
|
|
29
|
-
{ field: 'name', type: 'text', label: 'Product', required: true, minLength: 2 },
|
|
30
|
-
{ field: 'category_id', type: 'number', label: 'Category #', min: 1, max: 8 },
|
|
31
|
-
{ field: 'unit_price', type: 'number', label: 'Unit Price ($)', min: 0 },
|
|
32
|
-
{ field: 'units_in_stock', type: 'number', label: 'In Stock', min: 0 },
|
|
33
|
-
{ field: 'discontinued', type: 'boolean' },
|
|
34
|
-
],
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const customers: EntitySchema = {
|
|
38
|
-
name: 'customers',
|
|
39
|
-
label: 'Customer',
|
|
40
|
-
idField: 'id',
|
|
41
|
-
fields: [
|
|
42
|
-
{ field: 'id', type: 'number', primaryKey: true, readonly: true },
|
|
43
|
-
{ field: 'company', type: 'text', label: 'Company', required: true, minLength: 2 },
|
|
44
|
-
{ field: 'contact', type: 'text', label: 'Contact' },
|
|
45
|
-
{ field: 'city', type: 'text', label: 'City' },
|
|
46
|
-
{ field: 'country', type: 'text', label: 'Country' },
|
|
47
|
-
],
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** The `order_lines` join view (Supabase) - read-only. */
|
|
51
|
-
const orderLines: EntitySchema = {
|
|
52
|
-
name: 'order_lines',
|
|
53
|
-
label: 'Order line',
|
|
54
|
-
idField: 'id',
|
|
55
|
-
fields: [
|
|
56
|
-
{ field: 'id', type: 'number', primaryKey: true, readonly: true },
|
|
57
|
-
{ field: 'order_id', type: 'number', label: 'Order #', readonly: true },
|
|
58
|
-
{ field: 'order_date', type: 'dateString', label: 'Date', readonly: true },
|
|
59
|
-
{ field: 'customer', type: 'text', label: 'Customer', readonly: true },
|
|
60
|
-
{ field: 'product', type: 'text', label: 'Product', readonly: true },
|
|
61
|
-
{ field: 'category', type: 'text', label: 'Category', readonly: true },
|
|
62
|
-
{ field: 'quantity', type: 'number', label: 'Qty', readonly: true },
|
|
63
|
-
{ field: 'unit_price', type: 'number', label: 'Unit Price ($)', readonly: true },
|
|
64
|
-
{ field: 'line_total', type: 'number', label: 'Line Total ($)', readonly: true },
|
|
65
|
-
{ field: 'ship_country', type: 'text', label: 'Ship To', readonly: true },
|
|
66
|
-
],
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ---- Curated Northwind seed (for the PGlite starter) ------------------------
|
|
70
|
-
|
|
71
|
-
const PRODUCTS: Row[] = [
|
|
72
|
-
{ id: 1, name: 'Chai', category_id: 1, unit_price: 18, units_in_stock: 39, discontinued: false },
|
|
73
|
-
{ id: 2, name: 'Chang', category_id: 1, unit_price: 19, units_in_stock: 17, discontinued: false },
|
|
74
|
-
{ id: 3, name: 'Aniseed Syrup', category_id: 2, unit_price: 10, units_in_stock: 13, discontinued: false },
|
|
75
|
-
{ id: 4, name: "Chef Anton's Cajun Seasoning", category_id: 2, unit_price: 22, units_in_stock: 53, discontinued: false },
|
|
76
|
-
{ id: 5, name: "Grandma's Boysenberry Spread", category_id: 2, unit_price: 25, units_in_stock: 120, discontinued: false },
|
|
77
|
-
{ id: 6, name: "Uncle Bob's Organic Dried Pears", category_id: 7, unit_price: 30, units_in_stock: 15, discontinued: false },
|
|
78
|
-
{ id: 7, name: 'Northwoods Cranberry Sauce', category_id: 2, unit_price: 40, units_in_stock: 6, discontinued: false },
|
|
79
|
-
{ id: 8, name: 'Mishi Kobe Niku', category_id: 6, unit_price: 97, units_in_stock: 29, discontinued: true },
|
|
80
|
-
{ id: 9, name: 'Ikura', category_id: 8, unit_price: 31, units_in_stock: 31, discontinued: false },
|
|
81
|
-
{ id: 10, name: 'Queso Cabrales', category_id: 4, unit_price: 21, units_in_stock: 22, discontinued: false },
|
|
82
|
-
{ id: 11, name: 'Konbu', category_id: 8, unit_price: 6, units_in_stock: 24, discontinued: false },
|
|
83
|
-
{ id: 12, name: 'Tofu', category_id: 7, unit_price: 23.25, units_in_stock: 35, discontinued: false },
|
|
84
|
-
{ id: 13, name: 'Genen Shouyu', category_id: 2, unit_price: 15.5, units_in_stock: 39, discontinued: false },
|
|
85
|
-
{ id: 14, name: 'Pavlova', category_id: 3, unit_price: 17.45, units_in_stock: 29, discontinued: false },
|
|
86
|
-
{ id: 15, name: 'Alice Mutton', category_id: 6, unit_price: 39, units_in_stock: 0, discontinued: true },
|
|
87
|
-
{ id: 16, name: 'Carnarvon Tigers', category_id: 8, unit_price: 62.5, units_in_stock: 42, discontinued: false },
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
const CUSTOMERS: Row[] = [
|
|
91
|
-
{ id: 1, company: 'Alfreds Futterkiste', contact: 'Maria Anders', city: 'Berlin', country: 'Germany' },
|
|
92
|
-
{ id: 2, company: 'Ana Trujillo Emparedados', contact: 'Ana Trujillo', city: 'México D.F.', country: 'Mexico' },
|
|
93
|
-
{ id: 3, company: 'Antonio Moreno Taquería', contact: 'Antonio Moreno', city: 'México D.F.', country: 'Mexico' },
|
|
94
|
-
{ id: 4, company: 'Around the Horn', contact: 'Thomas Hardy', city: 'London', country: 'UK' },
|
|
95
|
-
{ id: 5, company: 'Berglunds snabbköp', contact: 'Christina Berglund', city: 'Luleå', country: 'Sweden' },
|
|
96
|
-
{ id: 6, company: 'Blauer See Delikatessen', contact: 'Hanna Moos', city: 'Mannheim', country: 'Germany' },
|
|
97
|
-
{ id: 7, company: 'Bólido Comidas preparadas', contact: 'Martín Sommer', city: 'Madrid', country: 'Spain' },
|
|
98
|
-
{ id: 8, company: "Bon app'", contact: 'Laurence Lebihan', city: 'Marseille', country: 'France' },
|
|
99
|
-
{ id: 9, company: 'Bottom-Dollar Markets', contact: 'Elizabeth Lincoln', city: 'Tsawassen', country: 'Canada' },
|
|
100
|
-
{ id: 10, company: "B's Beverages", contact: 'Victoria Ashworth', city: 'London', country: 'UK' },
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
// ---- Hosted Supabase sample (see docs/enterprise/studio/supabase-sample.md) --
|
|
104
|
-
// Read-only anon RLS, so the publishable key is safe to embed.
|
|
105
|
-
const SUPABASE_URL = 'https://rbnnlzgtfzsylllniozo.supabase.co'
|
|
106
|
-
const SUPABASE_KEY = 'sb_publishable_SiT7CxGl4Z_Du1opTYbxVA_VQWumPUN'
|
|
107
|
-
|
|
108
|
-
/** Assemble a starter project from entities + screens + explicit per-entity sources. */
|
|
109
|
-
function assemble(opts: {
|
|
110
|
-
title: string
|
|
111
|
-
brand: string
|
|
112
|
-
accent: string
|
|
113
|
-
mode?: 'light' | 'dark'
|
|
114
|
-
preset?: string
|
|
115
|
-
navStyle?:
|
|
116
|
-
entities: EntitySchema[]
|
|
117
|
-
screens: Screen[]
|
|
118
|
-
sources: Record<string, EntityDataSource>
|
|
119
|
-
dataSource: StudioProject['dataSource']
|
|
120
|
-
}): StudioProject {
|
|
121
|
-
return sanitizeProject({
|
|
122
|
-
title: opts.title,
|
|
123
|
-
entities: opts.entities,
|
|
124
|
-
screens: opts.screens,
|
|
125
|
-
dataSource: opts.dataSource,
|
|
126
|
-
dataSources: opts.sources,
|
|
127
|
-
theme: {
|
|
128
|
-
accent: opts.accent,
|
|
129
|
-
...(opts.mode ? { mode: opts.mode } : {}),
|
|
130
|
-
...(opts.preset ? { preset: opts.preset } : {}),
|
|
131
|
-
shell: { style: opts.navStyle ?? 'sidebar', brand: opts.brand, footer: '', navPosition: 'left' },
|
|
132
|
-
},
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---- Starters ---------------------------------------------------------------
|
|
137
|
-
|
|
138
|
-
/** Northwind on embedded Postgres (PGlite) - real SQL + CRUD, zero backend. */
|
|
139
|
-
const northwindPglite: SampleApp = {
|
|
140
|
-
id: 'live-northwind-pglite',
|
|
141
|
-
name: 'Northwind · Local Postgres',
|
|
142
|
-
description: 'The Northwind sample on a real in-browser Postgres (PGlite). Full CRUD, no server, no keys.',
|
|
143
|
-
emoji: '🐘',
|
|
144
|
-
accent: '#2f6f4f',
|
|
145
|
-
build: () =>
|
|
146
|
-
assemble({
|
|
147
|
-
title: 'Northwind (Local Postgres)',
|
|
148
|
-
brand: 'Northwind',
|
|
149
|
-
accent: '#2f6f4f',
|
|
150
|
-
preset: 'material',
|
|
151
|
-
dataSource: 'pglite',
|
|
152
|
-
entities: [products, customers],
|
|
153
|
-
screens: [
|
|
154
|
-
dashScreen(products, { id: 'overview', title: 'Overview', order: 0 }, [
|
|
155
|
-
{ kpi: 'Products', reduce: 'count', span: 1 },
|
|
156
|
-
{ kpi: 'Avg price', measure: 'unit_price', reduce: 'avg', format: 'currency', span: 1 },
|
|
157
|
-
{ kpi: 'In stock', measure: 'units_in_stock', reduce: 'sum', span: 1 },
|
|
158
|
-
{ kpi: 'Priciest', measure: 'unit_price', reduce: 'max', format: 'currency', span: 1 },
|
|
159
|
-
{ chart: 'category_id', measure: 'unit_price', reduce: 'sum', type: 'bar', span: 2 },
|
|
160
|
-
{ chart: 'discontinued', reduce: 'count', type: 'pie', span: 1 },
|
|
161
|
-
{ grid: true, rowLink: { screen: 'product-detail', sourceField: 'id', targetField: 'id' }, span: 3 },
|
|
162
|
-
]),
|
|
163
|
-
detailScreen(products, { id: 'product-detail', title: 'Product', order: 1 }, {
|
|
164
|
-
titleField: 'name', metricFields: ['unit_price', 'units_in_stock'],
|
|
165
|
-
sections: [{ label: 'Catalog', fields: ['category_id', 'discontinued'] }],
|
|
166
|
-
}),
|
|
167
|
-
listScreen(customers, { id: 'customers', title: 'Customers', order: 2 }, { grid: { rowLink: { screen: 'customer-detail', sourceField: 'id', targetField: 'id' } } }),
|
|
168
|
-
detailScreen(customers, { id: 'customer-detail', title: 'Customer', order: 3 }, {
|
|
169
|
-
titleField: 'company', subtitleField: 'contact',
|
|
170
|
-
sections: [{ label: 'Location', fields: ['city', 'country'] }],
|
|
171
|
-
}),
|
|
172
|
-
],
|
|
173
|
-
sources: {
|
|
174
|
-
products: { kind: 'pglite', table: 'products', seed: PRODUCTS },
|
|
175
|
-
customers: { kind: 'pglite', table: 'customers', seed: CUSTOMERS },
|
|
176
|
-
},
|
|
177
|
-
}),
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Northwind on a hosted Supabase project - live PostgREST, incl. the join view. */
|
|
181
|
-
const northwindSupabase: SampleApp = {
|
|
182
|
-
id: 'live-northwind-supabase',
|
|
183
|
-
name: 'Northwind · Supabase',
|
|
184
|
-
description: 'A hosted, read-only Northwind on Supabase. Browse the five-table order_lines join view live over PostgREST.',
|
|
185
|
-
emoji: '⚡',
|
|
186
|
-
accent: '#3ecf8e',
|
|
187
|
-
build: () =>
|
|
188
|
-
assemble({
|
|
189
|
-
title: 'Northwind (Supabase)',
|
|
190
|
-
brand: 'Northwind',
|
|
191
|
-
accent: '#3ecf8e',
|
|
192
|
-
mode: 'dark',
|
|
193
|
-
preset: 'vercel',
|
|
194
|
-
dataSource: 'supabase',
|
|
195
|
-
entities: [orderLines, products, customers],
|
|
196
|
-
screens: [
|
|
197
|
-
// Sales dashboard over the join view: revenue KPIs + breakdowns by category /
|
|
198
|
-
// country / product, a faceted filter, and the raw order-lines grid.
|
|
199
|
-
dashScreen(orderLines, { id: 'sales', title: 'Sales', order: 0 }, [
|
|
200
|
-
{ kpi: 'Line items', reduce: 'count', span: 1 },
|
|
201
|
-
{ kpi: 'Revenue', measure: 'line_total', reduce: 'sum', format: 'currency', span: 1 },
|
|
202
|
-
{ kpi: 'Avg line', measure: 'line_total', reduce: 'avg', format: 'currency', span: 1 },
|
|
203
|
-
{ kpi: 'Units sold', measure: 'quantity', reduce: 'sum', span: 1 },
|
|
204
|
-
{ chart: 'category', measure: 'line_total', reduce: 'sum', type: 'bar', span: 2 },
|
|
205
|
-
{ chart: 'ship_country', measure: 'line_total', reduce: 'sum', type: 'pie', span: 1 },
|
|
206
|
-
{ chart: 'product', measure: 'line_total', reduce: 'sum', type: 'bar', span: 3 },
|
|
207
|
-
{ filter: ['category', 'ship_country'], span: 3 },
|
|
208
|
-
{ grid: true, span: 3 },
|
|
209
|
-
]),
|
|
210
|
-
listScreen(products, { id: 'products', title: 'Products', order: 1 }, { grid: { rowLink: { screen: 'product-360', sourceField: 'id', targetField: 'id' } } }),
|
|
211
|
-
// Product 360: unit price / stock tiles + a timeline of the order lines for
|
|
212
|
-
// this product. The view has no FK ids, so the timeline matches by name.
|
|
213
|
-
detailScreen(products, { id: 'product-360', title: 'Product', order: 2 }, {
|
|
214
|
-
titleField: 'name', metricFields: ['unit_price', 'units_in_stock'],
|
|
215
|
-
sections: [{ label: 'Catalog', fields: ['category_id', 'discontinued'] }],
|
|
216
|
-
related: [{ entity: 'order_lines', foreignKey: 'product', parentField: 'name', label: 'Orders', titleField: 'customer', subtitleField: 'line_total', dateField: 'order_date' }],
|
|
217
|
-
}),
|
|
218
|
-
listScreen(customers, { id: 'customers', title: 'Customers', order: 3 }, { grid: { rowLink: { screen: 'customer-360', sourceField: 'id', targetField: 'id' } } }),
|
|
219
|
-
// Customer 360: an order-history timeline matched by company name.
|
|
220
|
-
detailScreen(customers, { id: 'customer-360', title: 'Customer', order: 4 }, {
|
|
221
|
-
titleField: 'company', subtitleField: 'contact',
|
|
222
|
-
sections: [{ label: 'Location', fields: ['city', 'country'] }],
|
|
223
|
-
related: [{ entity: 'order_lines', foreignKey: 'customer', parentField: 'company', label: 'Orders', titleField: 'product', subtitleField: 'line_total', dateField: 'order_date' }],
|
|
224
|
-
}),
|
|
225
|
-
],
|
|
226
|
-
sources: {
|
|
227
|
-
order_lines: { kind: 'supabase', table: 'order_lines', url: SUPABASE_URL, key: SUPABASE_KEY },
|
|
228
|
-
products: { kind: 'supabase', table: 'products', url: SUPABASE_URL, key: SUPABASE_KEY },
|
|
229
|
-
customers: { kind: 'supabase', table: 'customers', url: SUPABASE_URL, key: SUPABASE_KEY },
|
|
230
|
-
},
|
|
231
|
-
}),
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** A live public REST feed (DummyJSON products) - no backend, no keys. */
|
|
235
|
-
const dummyjsonRest: SampleApp = {
|
|
236
|
-
id: 'live-dummyjson-rest',
|
|
237
|
-
name: 'Products · Live REST',
|
|
238
|
-
description: 'A live product feed from the public DummyJSON API over createRestDataSource. No backend, no keys.',
|
|
239
|
-
emoji: '🛰️',
|
|
240
|
-
accent: '#6d5efc',
|
|
241
|
-
build: () => {
|
|
242
|
-
const restProduct: EntitySchema = {
|
|
243
|
-
name: 'products',
|
|
244
|
-
label: 'Product',
|
|
245
|
-
idField: 'id',
|
|
246
|
-
fields: [
|
|
247
|
-
{ field: 'id', type: 'number', primaryKey: true, readonly: true },
|
|
248
|
-
{ field: 'title', type: 'text', label: 'Product' },
|
|
249
|
-
{ field: 'brand', type: 'text', label: 'Brand' },
|
|
250
|
-
{ field: 'category', type: 'text', label: 'Category' },
|
|
251
|
-
{ field: 'price', type: 'number', label: 'Price ($)' },
|
|
252
|
-
{ field: 'rating', type: 'number', label: 'Rating' },
|
|
253
|
-
{ field: 'stock', type: 'number', label: 'Stock' },
|
|
254
|
-
],
|
|
255
|
-
}
|
|
256
|
-
return assemble({
|
|
257
|
-
title: 'Live Products (REST)',
|
|
258
|
-
brand: 'Live Feed',
|
|
259
|
-
accent: '#6d5efc',
|
|
260
|
-
preset: 'shadcn',
|
|
261
|
-
dataSource: 'rest',
|
|
262
|
-
entities: [restProduct],
|
|
263
|
-
// A page size that covers the whole catalogue: the grid asks for one page,
|
|
264
|
-
// so it reads the entire live feed in a single request. (DummyJSON pages by
|
|
265
|
-
// `skip`, not `offset`, so deep server-paging would need a REST adapter -
|
|
266
|
-
// see docs/enterprise/studio/rest-api.md.)
|
|
267
|
-
screens: [
|
|
268
|
-
// Catalog dashboard: price / rating / stock KPIs + category & brand breakdowns
|
|
269
|
-
// over the live feed, with the grid drilling into a product page.
|
|
270
|
-
dashScreen(restProduct, { id: 'catalog', title: 'Catalog', order: 0 }, [
|
|
271
|
-
{ kpi: 'Products', reduce: 'count', span: 1 },
|
|
272
|
-
{ kpi: 'Avg price', measure: 'price', reduce: 'avg', format: 'currency', span: 1 },
|
|
273
|
-
{ kpi: 'Avg rating', measure: 'rating', reduce: 'avg', span: 1 },
|
|
274
|
-
{ kpi: 'In stock', measure: 'stock', reduce: 'sum', span: 1 },
|
|
275
|
-
{ chart: 'category', reduce: 'count', type: 'bar', span: 2 },
|
|
276
|
-
{ chart: 'brand', measure: 'price', reduce: 'avg', type: 'bar', span: 1 },
|
|
277
|
-
{ filter: ['category', 'brand'], span: 3 },
|
|
278
|
-
{ grid: true, pageSize: 200, rowLink: { screen: 'product-detail', sourceField: 'id', targetField: 'id' }, span: 3 },
|
|
279
|
-
]),
|
|
280
|
-
detailScreen(restProduct, { id: 'product-detail', title: 'Product', order: 1 }, {
|
|
281
|
-
titleField: 'title', subtitleField: 'brand', metricFields: ['price', 'rating', 'stock'],
|
|
282
|
-
sections: [{ label: 'Details', fields: ['category', 'brand'] }],
|
|
283
|
-
}),
|
|
284
|
-
],
|
|
285
|
-
sources: {
|
|
286
|
-
// Rows + total read from DummyJSON's { products, total } envelope. `limit=0`
|
|
287
|
-
// asks DummyJSON for the whole catalogue so the dashboard aggregates it all.
|
|
288
|
-
products: {
|
|
289
|
-
kind: 'rest',
|
|
290
|
-
baseUrl: 'https://dummyjson.com',
|
|
291
|
-
path: 'products',
|
|
292
|
-
method: 'GET',
|
|
293
|
-
params: [{ name: 'limit', location: 'query', type: 'number', value: '0' }],
|
|
294
|
-
rowsPath: 'products',
|
|
295
|
-
totalPath: 'total',
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
})
|
|
299
|
-
},
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Starters wired to a real data source, in gallery order. */
|
|
303
|
-
export const liveDataSamples: SampleApp[] = [northwindPglite, northwindSupabase, dummyjsonRest]
|
|
304
|
-
|
|
305
|
-
/** Look up a live-data starter by id. */
|
|
306
|
-
export function getLiveDataSample(id: string): SampleApp | undefined {
|
|
307
|
-
return liveDataSamples.find((s) => s.id === id)
|
|
308
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* "Start from live data" gallery: starter `StudioProject`s wired to a REAL data
|
|
3
|
+
* source instead of an in-memory seed - so one click gives you an app that talks
|
|
4
|
+
* to actual Postgres or a live API, then generates a runnable project bound to
|
|
5
|
+
* the same source.
|
|
6
|
+
*
|
|
7
|
+
* These are deliberately separate from `sampleApps` (which are polished,
|
|
8
|
+
* memory-backed, enterprise-layout showcases). A live-data starter's value is
|
|
9
|
+
* the *binding*: embedded Postgres (PGlite), a hosted Supabase project, or a
|
|
10
|
+
* public REST API.
|
|
11
|
+
*
|
|
12
|
+
* Pure + node-safe: plain data built from the same ops the designer uses, so
|
|
13
|
+
* each round-trips through parse/serialize and generates like any project.
|
|
14
|
+
*/
|
|
15
|
+
import type { EntitySchema } from '../../schema.js'
|
|
16
|
+
import { listScreen, dashScreen, detailScreen, type SampleApp } from './shared.js'
|
|
17
|
+
import { sanitizeProject, type EntityDataSource, type Screen, type StudioProject, type ShellStyle } from '../project.js'
|
|
18
|
+
|
|
19
|
+
type Row = Record<string, unknown>
|
|
20
|
+
|
|
21
|
+
// ---- Northwind schemas (shared by the PGlite + Supabase starters) ----------
|
|
22
|
+
|
|
23
|
+
const products: EntitySchema = {
|
|
24
|
+
name: 'products',
|
|
25
|
+
label: 'Product',
|
|
26
|
+
idField: 'id',
|
|
27
|
+
fields: [
|
|
28
|
+
{ field: 'id', type: 'number', primaryKey: true, readonly: true },
|
|
29
|
+
{ field: 'name', type: 'text', label: 'Product', required: true, minLength: 2 },
|
|
30
|
+
{ field: 'category_id', type: 'number', label: 'Category #', min: 1, max: 8 },
|
|
31
|
+
{ field: 'unit_price', type: 'number', label: 'Unit Price ($)', min: 0 },
|
|
32
|
+
{ field: 'units_in_stock', type: 'number', label: 'In Stock', min: 0 },
|
|
33
|
+
{ field: 'discontinued', type: 'boolean' },
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const customers: EntitySchema = {
|
|
38
|
+
name: 'customers',
|
|
39
|
+
label: 'Customer',
|
|
40
|
+
idField: 'id',
|
|
41
|
+
fields: [
|
|
42
|
+
{ field: 'id', type: 'number', primaryKey: true, readonly: true },
|
|
43
|
+
{ field: 'company', type: 'text', label: 'Company', required: true, minLength: 2 },
|
|
44
|
+
{ field: 'contact', type: 'text', label: 'Contact' },
|
|
45
|
+
{ field: 'city', type: 'text', label: 'City' },
|
|
46
|
+
{ field: 'country', type: 'text', label: 'Country' },
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The `order_lines` join view (Supabase) - read-only. */
|
|
51
|
+
const orderLines: EntitySchema = {
|
|
52
|
+
name: 'order_lines',
|
|
53
|
+
label: 'Order line',
|
|
54
|
+
idField: 'id',
|
|
55
|
+
fields: [
|
|
56
|
+
{ field: 'id', type: 'number', primaryKey: true, readonly: true },
|
|
57
|
+
{ field: 'order_id', type: 'number', label: 'Order #', readonly: true },
|
|
58
|
+
{ field: 'order_date', type: 'dateString', label: 'Date', readonly: true },
|
|
59
|
+
{ field: 'customer', type: 'text', label: 'Customer', readonly: true },
|
|
60
|
+
{ field: 'product', type: 'text', label: 'Product', readonly: true },
|
|
61
|
+
{ field: 'category', type: 'text', label: 'Category', readonly: true },
|
|
62
|
+
{ field: 'quantity', type: 'number', label: 'Qty', readonly: true },
|
|
63
|
+
{ field: 'unit_price', type: 'number', label: 'Unit Price ($)', readonly: true },
|
|
64
|
+
{ field: 'line_total', type: 'number', label: 'Line Total ($)', readonly: true },
|
|
65
|
+
{ field: 'ship_country', type: 'text', label: 'Ship To', readonly: true },
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- Curated Northwind seed (for the PGlite starter) ------------------------
|
|
70
|
+
|
|
71
|
+
const PRODUCTS: Row[] = [
|
|
72
|
+
{ id: 1, name: 'Chai', category_id: 1, unit_price: 18, units_in_stock: 39, discontinued: false },
|
|
73
|
+
{ id: 2, name: 'Chang', category_id: 1, unit_price: 19, units_in_stock: 17, discontinued: false },
|
|
74
|
+
{ id: 3, name: 'Aniseed Syrup', category_id: 2, unit_price: 10, units_in_stock: 13, discontinued: false },
|
|
75
|
+
{ id: 4, name: "Chef Anton's Cajun Seasoning", category_id: 2, unit_price: 22, units_in_stock: 53, discontinued: false },
|
|
76
|
+
{ id: 5, name: "Grandma's Boysenberry Spread", category_id: 2, unit_price: 25, units_in_stock: 120, discontinued: false },
|
|
77
|
+
{ id: 6, name: "Uncle Bob's Organic Dried Pears", category_id: 7, unit_price: 30, units_in_stock: 15, discontinued: false },
|
|
78
|
+
{ id: 7, name: 'Northwoods Cranberry Sauce', category_id: 2, unit_price: 40, units_in_stock: 6, discontinued: false },
|
|
79
|
+
{ id: 8, name: 'Mishi Kobe Niku', category_id: 6, unit_price: 97, units_in_stock: 29, discontinued: true },
|
|
80
|
+
{ id: 9, name: 'Ikura', category_id: 8, unit_price: 31, units_in_stock: 31, discontinued: false },
|
|
81
|
+
{ id: 10, name: 'Queso Cabrales', category_id: 4, unit_price: 21, units_in_stock: 22, discontinued: false },
|
|
82
|
+
{ id: 11, name: 'Konbu', category_id: 8, unit_price: 6, units_in_stock: 24, discontinued: false },
|
|
83
|
+
{ id: 12, name: 'Tofu', category_id: 7, unit_price: 23.25, units_in_stock: 35, discontinued: false },
|
|
84
|
+
{ id: 13, name: 'Genen Shouyu', category_id: 2, unit_price: 15.5, units_in_stock: 39, discontinued: false },
|
|
85
|
+
{ id: 14, name: 'Pavlova', category_id: 3, unit_price: 17.45, units_in_stock: 29, discontinued: false },
|
|
86
|
+
{ id: 15, name: 'Alice Mutton', category_id: 6, unit_price: 39, units_in_stock: 0, discontinued: true },
|
|
87
|
+
{ id: 16, name: 'Carnarvon Tigers', category_id: 8, unit_price: 62.5, units_in_stock: 42, discontinued: false },
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
const CUSTOMERS: Row[] = [
|
|
91
|
+
{ id: 1, company: 'Alfreds Futterkiste', contact: 'Maria Anders', city: 'Berlin', country: 'Germany' },
|
|
92
|
+
{ id: 2, company: 'Ana Trujillo Emparedados', contact: 'Ana Trujillo', city: 'México D.F.', country: 'Mexico' },
|
|
93
|
+
{ id: 3, company: 'Antonio Moreno Taquería', contact: 'Antonio Moreno', city: 'México D.F.', country: 'Mexico' },
|
|
94
|
+
{ id: 4, company: 'Around the Horn', contact: 'Thomas Hardy', city: 'London', country: 'UK' },
|
|
95
|
+
{ id: 5, company: 'Berglunds snabbköp', contact: 'Christina Berglund', city: 'Luleå', country: 'Sweden' },
|
|
96
|
+
{ id: 6, company: 'Blauer See Delikatessen', contact: 'Hanna Moos', city: 'Mannheim', country: 'Germany' },
|
|
97
|
+
{ id: 7, company: 'Bólido Comidas preparadas', contact: 'Martín Sommer', city: 'Madrid', country: 'Spain' },
|
|
98
|
+
{ id: 8, company: "Bon app'", contact: 'Laurence Lebihan', city: 'Marseille', country: 'France' },
|
|
99
|
+
{ id: 9, company: 'Bottom-Dollar Markets', contact: 'Elizabeth Lincoln', city: 'Tsawassen', country: 'Canada' },
|
|
100
|
+
{ id: 10, company: "B's Beverages", contact: 'Victoria Ashworth', city: 'London', country: 'UK' },
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
// ---- Hosted Supabase sample (see docs/enterprise/studio/supabase-sample.md) --
|
|
104
|
+
// Read-only anon RLS, so the publishable key is safe to embed.
|
|
105
|
+
const SUPABASE_URL = 'https://rbnnlzgtfzsylllniozo.supabase.co'
|
|
106
|
+
const SUPABASE_KEY = 'sb_publishable_SiT7CxGl4Z_Du1opTYbxVA_VQWumPUN'
|
|
107
|
+
|
|
108
|
+
/** Assemble a starter project from entities + screens + explicit per-entity sources. */
|
|
109
|
+
function assemble(opts: {
|
|
110
|
+
title: string
|
|
111
|
+
brand: string
|
|
112
|
+
accent: string
|
|
113
|
+
mode?: 'light' | 'dark'
|
|
114
|
+
preset?: string
|
|
115
|
+
navStyle?: ShellStyle
|
|
116
|
+
entities: EntitySchema[]
|
|
117
|
+
screens: Screen[]
|
|
118
|
+
sources: Record<string, EntityDataSource>
|
|
119
|
+
dataSource: StudioProject['dataSource']
|
|
120
|
+
}): StudioProject {
|
|
121
|
+
return sanitizeProject({
|
|
122
|
+
title: opts.title,
|
|
123
|
+
entities: opts.entities,
|
|
124
|
+
screens: opts.screens,
|
|
125
|
+
dataSource: opts.dataSource,
|
|
126
|
+
dataSources: opts.sources,
|
|
127
|
+
theme: {
|
|
128
|
+
accent: opts.accent,
|
|
129
|
+
...(opts.mode ? { mode: opts.mode } : {}),
|
|
130
|
+
...(opts.preset ? { preset: opts.preset } : {}),
|
|
131
|
+
shell: { style: opts.navStyle ?? 'sidebar', brand: opts.brand, footer: '', navPosition: 'left' },
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- Starters ---------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/** Northwind on embedded Postgres (PGlite) - real SQL + CRUD, zero backend. */
|
|
139
|
+
const northwindPglite: SampleApp = {
|
|
140
|
+
id: 'live-northwind-pglite',
|
|
141
|
+
name: 'Northwind · Local Postgres',
|
|
142
|
+
description: 'The Northwind sample on a real in-browser Postgres (PGlite). Full CRUD, no server, no keys.',
|
|
143
|
+
emoji: '🐘',
|
|
144
|
+
accent: '#2f6f4f',
|
|
145
|
+
build: () =>
|
|
146
|
+
assemble({
|
|
147
|
+
title: 'Northwind (Local Postgres)',
|
|
148
|
+
brand: 'Northwind',
|
|
149
|
+
accent: '#2f6f4f',
|
|
150
|
+
preset: 'material',
|
|
151
|
+
dataSource: 'pglite',
|
|
152
|
+
entities: [products, customers],
|
|
153
|
+
screens: [
|
|
154
|
+
dashScreen(products, { id: 'overview', title: 'Overview', order: 0 }, [
|
|
155
|
+
{ kpi: 'Products', reduce: 'count', span: 1 },
|
|
156
|
+
{ kpi: 'Avg price', measure: 'unit_price', reduce: 'avg', format: 'currency', span: 1 },
|
|
157
|
+
{ kpi: 'In stock', measure: 'units_in_stock', reduce: 'sum', span: 1 },
|
|
158
|
+
{ kpi: 'Priciest', measure: 'unit_price', reduce: 'max', format: 'currency', span: 1 },
|
|
159
|
+
{ chart: 'category_id', measure: 'unit_price', reduce: 'sum', type: 'bar', span: 2 },
|
|
160
|
+
{ chart: 'discontinued', reduce: 'count', type: 'pie', span: 1 },
|
|
161
|
+
{ grid: true, rowLink: { screen: 'product-detail', sourceField: 'id', targetField: 'id' }, span: 3 },
|
|
162
|
+
]),
|
|
163
|
+
detailScreen(products, { id: 'product-detail', title: 'Product', order: 1 }, {
|
|
164
|
+
titleField: 'name', metricFields: ['unit_price', 'units_in_stock'],
|
|
165
|
+
sections: [{ label: 'Catalog', fields: ['category_id', 'discontinued'] }],
|
|
166
|
+
}),
|
|
167
|
+
listScreen(customers, { id: 'customers', title: 'Customers', order: 2 }, { grid: { rowLink: { screen: 'customer-detail', sourceField: 'id', targetField: 'id' } } }),
|
|
168
|
+
detailScreen(customers, { id: 'customer-detail', title: 'Customer', order: 3 }, {
|
|
169
|
+
titleField: 'company', subtitleField: 'contact',
|
|
170
|
+
sections: [{ label: 'Location', fields: ['city', 'country'] }],
|
|
171
|
+
}),
|
|
172
|
+
],
|
|
173
|
+
sources: {
|
|
174
|
+
products: { kind: 'pglite', table: 'products', seed: PRODUCTS },
|
|
175
|
+
customers: { kind: 'pglite', table: 'customers', seed: CUSTOMERS },
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Northwind on a hosted Supabase project - live PostgREST, incl. the join view. */
|
|
181
|
+
const northwindSupabase: SampleApp = {
|
|
182
|
+
id: 'live-northwind-supabase',
|
|
183
|
+
name: 'Northwind · Supabase',
|
|
184
|
+
description: 'A hosted, read-only Northwind on Supabase. Browse the five-table order_lines join view live over PostgREST.',
|
|
185
|
+
emoji: '⚡',
|
|
186
|
+
accent: '#3ecf8e',
|
|
187
|
+
build: () =>
|
|
188
|
+
assemble({
|
|
189
|
+
title: 'Northwind (Supabase)',
|
|
190
|
+
brand: 'Northwind',
|
|
191
|
+
accent: '#3ecf8e',
|
|
192
|
+
mode: 'dark',
|
|
193
|
+
preset: 'vercel',
|
|
194
|
+
dataSource: 'supabase',
|
|
195
|
+
entities: [orderLines, products, customers],
|
|
196
|
+
screens: [
|
|
197
|
+
// Sales dashboard over the join view: revenue KPIs + breakdowns by category /
|
|
198
|
+
// country / product, a faceted filter, and the raw order-lines grid.
|
|
199
|
+
dashScreen(orderLines, { id: 'sales', title: 'Sales', order: 0 }, [
|
|
200
|
+
{ kpi: 'Line items', reduce: 'count', span: 1 },
|
|
201
|
+
{ kpi: 'Revenue', measure: 'line_total', reduce: 'sum', format: 'currency', span: 1 },
|
|
202
|
+
{ kpi: 'Avg line', measure: 'line_total', reduce: 'avg', format: 'currency', span: 1 },
|
|
203
|
+
{ kpi: 'Units sold', measure: 'quantity', reduce: 'sum', span: 1 },
|
|
204
|
+
{ chart: 'category', measure: 'line_total', reduce: 'sum', type: 'bar', span: 2 },
|
|
205
|
+
{ chart: 'ship_country', measure: 'line_total', reduce: 'sum', type: 'pie', span: 1 },
|
|
206
|
+
{ chart: 'product', measure: 'line_total', reduce: 'sum', type: 'bar', span: 3 },
|
|
207
|
+
{ filter: ['category', 'ship_country'], span: 3 },
|
|
208
|
+
{ grid: true, span: 3 },
|
|
209
|
+
]),
|
|
210
|
+
listScreen(products, { id: 'products', title: 'Products', order: 1 }, { grid: { rowLink: { screen: 'product-360', sourceField: 'id', targetField: 'id' } } }),
|
|
211
|
+
// Product 360: unit price / stock tiles + a timeline of the order lines for
|
|
212
|
+
// this product. The view has no FK ids, so the timeline matches by name.
|
|
213
|
+
detailScreen(products, { id: 'product-360', title: 'Product', order: 2 }, {
|
|
214
|
+
titleField: 'name', metricFields: ['unit_price', 'units_in_stock'],
|
|
215
|
+
sections: [{ label: 'Catalog', fields: ['category_id', 'discontinued'] }],
|
|
216
|
+
related: [{ entity: 'order_lines', foreignKey: 'product', parentField: 'name', label: 'Orders', titleField: 'customer', subtitleField: 'line_total', dateField: 'order_date' }],
|
|
217
|
+
}),
|
|
218
|
+
listScreen(customers, { id: 'customers', title: 'Customers', order: 3 }, { grid: { rowLink: { screen: 'customer-360', sourceField: 'id', targetField: 'id' } } }),
|
|
219
|
+
// Customer 360: an order-history timeline matched by company name.
|
|
220
|
+
detailScreen(customers, { id: 'customer-360', title: 'Customer', order: 4 }, {
|
|
221
|
+
titleField: 'company', subtitleField: 'contact',
|
|
222
|
+
sections: [{ label: 'Location', fields: ['city', 'country'] }],
|
|
223
|
+
related: [{ entity: 'order_lines', foreignKey: 'customer', parentField: 'company', label: 'Orders', titleField: 'product', subtitleField: 'line_total', dateField: 'order_date' }],
|
|
224
|
+
}),
|
|
225
|
+
],
|
|
226
|
+
sources: {
|
|
227
|
+
order_lines: { kind: 'supabase', table: 'order_lines', url: SUPABASE_URL, key: SUPABASE_KEY },
|
|
228
|
+
products: { kind: 'supabase', table: 'products', url: SUPABASE_URL, key: SUPABASE_KEY },
|
|
229
|
+
customers: { kind: 'supabase', table: 'customers', url: SUPABASE_URL, key: SUPABASE_KEY },
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A live public REST feed (DummyJSON products) - no backend, no keys. */
|
|
235
|
+
const dummyjsonRest: SampleApp = {
|
|
236
|
+
id: 'live-dummyjson-rest',
|
|
237
|
+
name: 'Products · Live REST',
|
|
238
|
+
description: 'A live product feed from the public DummyJSON API over createRestDataSource. No backend, no keys.',
|
|
239
|
+
emoji: '🛰️',
|
|
240
|
+
accent: '#6d5efc',
|
|
241
|
+
build: () => {
|
|
242
|
+
const restProduct: EntitySchema = {
|
|
243
|
+
name: 'products',
|
|
244
|
+
label: 'Product',
|
|
245
|
+
idField: 'id',
|
|
246
|
+
fields: [
|
|
247
|
+
{ field: 'id', type: 'number', primaryKey: true, readonly: true },
|
|
248
|
+
{ field: 'title', type: 'text', label: 'Product' },
|
|
249
|
+
{ field: 'brand', type: 'text', label: 'Brand' },
|
|
250
|
+
{ field: 'category', type: 'text', label: 'Category' },
|
|
251
|
+
{ field: 'price', type: 'number', label: 'Price ($)' },
|
|
252
|
+
{ field: 'rating', type: 'number', label: 'Rating' },
|
|
253
|
+
{ field: 'stock', type: 'number', label: 'Stock' },
|
|
254
|
+
],
|
|
255
|
+
}
|
|
256
|
+
return assemble({
|
|
257
|
+
title: 'Live Products (REST)',
|
|
258
|
+
brand: 'Live Feed',
|
|
259
|
+
accent: '#6d5efc',
|
|
260
|
+
preset: 'shadcn',
|
|
261
|
+
dataSource: 'rest',
|
|
262
|
+
entities: [restProduct],
|
|
263
|
+
// A page size that covers the whole catalogue: the grid asks for one page,
|
|
264
|
+
// so it reads the entire live feed in a single request. (DummyJSON pages by
|
|
265
|
+
// `skip`, not `offset`, so deep server-paging would need a REST adapter -
|
|
266
|
+
// see docs/enterprise/studio/rest-api.md.)
|
|
267
|
+
screens: [
|
|
268
|
+
// Catalog dashboard: price / rating / stock KPIs + category & brand breakdowns
|
|
269
|
+
// over the live feed, with the grid drilling into a product page.
|
|
270
|
+
dashScreen(restProduct, { id: 'catalog', title: 'Catalog', order: 0 }, [
|
|
271
|
+
{ kpi: 'Products', reduce: 'count', span: 1 },
|
|
272
|
+
{ kpi: 'Avg price', measure: 'price', reduce: 'avg', format: 'currency', span: 1 },
|
|
273
|
+
{ kpi: 'Avg rating', measure: 'rating', reduce: 'avg', span: 1 },
|
|
274
|
+
{ kpi: 'In stock', measure: 'stock', reduce: 'sum', span: 1 },
|
|
275
|
+
{ chart: 'category', reduce: 'count', type: 'bar', span: 2 },
|
|
276
|
+
{ chart: 'brand', measure: 'price', reduce: 'avg', type: 'bar', span: 1 },
|
|
277
|
+
{ filter: ['category', 'brand'], span: 3 },
|
|
278
|
+
{ grid: true, pageSize: 200, rowLink: { screen: 'product-detail', sourceField: 'id', targetField: 'id' }, span: 3 },
|
|
279
|
+
]),
|
|
280
|
+
detailScreen(restProduct, { id: 'product-detail', title: 'Product', order: 1 }, {
|
|
281
|
+
titleField: 'title', subtitleField: 'brand', metricFields: ['price', 'rating', 'stock'],
|
|
282
|
+
sections: [{ label: 'Details', fields: ['category', 'brand'] }],
|
|
283
|
+
}),
|
|
284
|
+
],
|
|
285
|
+
sources: {
|
|
286
|
+
// Rows + total read from DummyJSON's { products, total } envelope. `limit=0`
|
|
287
|
+
// asks DummyJSON for the whole catalogue so the dashboard aggregates it all.
|
|
288
|
+
products: {
|
|
289
|
+
kind: 'rest',
|
|
290
|
+
baseUrl: 'https://dummyjson.com',
|
|
291
|
+
path: 'products',
|
|
292
|
+
method: 'GET',
|
|
293
|
+
params: [{ name: 'limit', location: 'query', type: 'number', value: '0' }],
|
|
294
|
+
rowsPath: 'products',
|
|
295
|
+
totalPath: 'total',
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Starters wired to a real data source, in gallery order. */
|
|
303
|
+
export const liveDataSamples: SampleApp[] = [northwindPglite, northwindSupabase, dummyjsonRest]
|
|
304
|
+
|
|
305
|
+
/** Look up a live-data starter by id. */
|
|
306
|
+
export function getLiveDataSample(id: string): SampleApp | undefined {
|
|
307
|
+
return liveDataSamples.find((s) => s.id === id)
|
|
308
|
+
}
|