@svgrid/enterprise 1.2.0 → 2.0.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/dist/cdn/svgrid-enterprise.svelte-external.js +26039 -805
- package/dist/designer/assets/index-Dp44bTid.js +939 -0
- package/dist/designer/assets/index-RJp6x8tw.css +1 -0
- package/dist/designer/assets/jszip.min-CjMo-QGg.js +2 -0
- package/dist/designer/index.html +13 -0
- package/dist/node/studio.js +22554 -0
- package/package.json +10 -3
- package/src/SvAuthGate.svelte +115 -0
- package/src/SvBoard.dom.test.ts +67 -0
- package/src/SvBoard.svelte +192 -0
- package/src/SvExportMenu.svelte +553 -0
- package/src/SvFileInput.svelte +113 -0
- package/src/SvGridEditPanel.dom.test.ts +146 -0
- package/src/SvGridEditPanel.svelte +793 -0
- package/src/SvGridMasterDetail.svelte +95 -0
- package/src/SvImportDialog.svelte +1020 -0
- package/src/SvLookupInput.svelte +180 -0
- package/src/SvRecordDetail.dom.test.ts +137 -0
- package/src/SvRecordDetail.svelte +288 -0
- package/src/SvSchedule.dom.test.ts +57 -0
- package/src/SvSchedule.svelte +156 -0
- package/src/SvSchemaChart.svelte +233 -0
- package/src/SvSchemaDashboard.svelte +130 -0
- package/src/ai-export-pdf.test.ts +74 -0
- package/src/ai-export-xlsx.test.ts +87 -0
- package/src/ai-export.test.ts +110 -0
- package/src/ai.ts +1188 -782
- package/src/edit-panel.test.ts +213 -0
- package/src/edit-panel.ts +228 -0
- package/src/export-conditional.test.ts +60 -0
- package/src/export-conditional.ts +94 -0
- package/src/export-ooxml.test.ts +152 -0
- package/src/export-ooxml.ts +485 -0
- package/src/export-pdf.test.ts +138 -0
- package/src/export-pdf.ts +245 -0
- package/src/export-print.test.ts +66 -0
- package/src/export-print.ts +132 -0
- package/src/export-serialize.test.ts +56 -0
- package/src/export-serialize.ts +24 -0
- package/src/export-xls.ts +196 -0
- package/src/export-xlsx-roundtrip.test.ts +120 -0
- package/src/export-xlsx.test.ts +150 -0
- package/src/export.test.ts +349 -0
- package/src/export.ts +1452 -549
- package/src/import-automap.test.ts +131 -0
- package/src/import-hardening.test.ts +158 -0
- package/src/import.ts +1042 -648
- package/src/index.ts +282 -0
- package/src/install.ts +134 -114
- package/src/license-core.test.ts +23 -0
- package/src/license-core.ts +35 -0
- package/src/license.ts +103 -90
- package/src/master-detail.test.ts +61 -0
- package/src/master-detail.ts +42 -0
- package/src/print.ts +107 -127
- package/src/schema-designer.test.ts +121 -0
- package/src/schema-designer.ts +142 -0
- package/src/schema.test.ts +268 -0
- package/src/schema.ts +499 -0
- package/src/smart-shim.ts +107 -105
- package/src/sources/aggregate.test.ts +79 -0
- package/src/sources/aggregate.ts +102 -0
- package/src/sources/auth-supabase.test.ts +80 -0
- package/src/sources/auth-supabase.ts +87 -0
- package/src/sources/dashboard.kpi.test.ts +50 -0
- package/src/sources/dashboard.test.ts +61 -0
- package/src/sources/dashboard.ts +136 -0
- package/src/sources/field-inference.test.ts +60 -0
- package/src/sources/field-inference.ts +63 -0
- package/src/sources/filters.test.ts +58 -0
- package/src/sources/filters.ts +64 -0
- package/src/sources/index.ts +67 -0
- package/src/sources/introspect-supabase.test.ts +100 -0
- package/src/sources/introspect-supabase.ts +119 -0
- package/src/sources/realtime-supabase.test.ts +93 -0
- package/src/sources/realtime-supabase.ts +99 -0
- package/src/sources/relation-lookup.test.ts +99 -0
- package/src/sources/relation-lookup.ts +128 -0
- package/src/sources/rest-adapters.test.ts +95 -0
- package/src/sources/rest-adapters.ts +120 -0
- package/src/sources/rest.test.ts +104 -0
- package/src/sources/rest.ts +129 -0
- package/src/sources/schema-from-columns.test.ts +106 -0
- package/src/sources/schema-from-columns.ts +88 -0
- package/src/sources/supabase.test.ts +110 -0
- package/src/sources/supabase.ts +99 -0
- package/src/sources/with-entity-rules.test.ts +104 -0
- package/src/sources/with-entity-rules.ts +78 -0
- package/src/sources/with-relation-labels.test.ts +75 -0
- package/src/sources/with-relation-labels.ts +73 -0
- package/src/studio/bug-report.test.ts +101 -0
- package/src/studio/bug-report.ts +165 -0
- package/src/studio/cli.test.ts +187 -0
- package/src/studio/cli.ts +112 -0
- package/src/studio/csv.test.ts +90 -0
- package/src/studio/csv.ts +165 -0
- package/src/studio/db-connect-string.test.ts +94 -0
- package/src/studio/db-connect-string.ts +128 -0
- package/src/studio/emit-project.test.ts +923 -0
- package/src/studio/emit-project.ts +1273 -0
- package/src/studio/emit-schema.test.ts +94 -0
- package/src/studio/emit-schema.ts +920 -0
- package/src/studio/index.ts +195 -0
- package/src/studio/introspect-db.test.ts +186 -0
- package/src/studio/introspect-db.ts +312 -0
- package/src/studio/introspect-prisma.test.ts +99 -0
- package/src/studio/introspect-prisma.ts +175 -0
- package/src/studio/introspect.test.ts +172 -0
- package/src/studio/introspect.ts +310 -0
- package/src/studio/pipeline.test.ts +42 -0
- package/src/studio/project-robust.test.ts +157 -0
- package/src/studio/project.test.ts +473 -0
- package/src/studio/project.ts +916 -0
- package/src/studio/sample-data.test.ts +58 -0
- package/src/studio/sample-data.ts +200 -0
- package/src/studio/samples/ats.ts +202 -0
- package/src/studio/samples/clinic.ts +177 -0
- package/src/studio/samples/crm.ts +250 -0
- package/src/studio/samples/ecommerce.ts +187 -0
- package/src/studio/samples/events.ts +199 -0
- package/src/studio/samples/fleet.ts +184 -0
- package/src/studio/samples/gym.ts +213 -0
- package/src/studio/samples/hr.ts +193 -0
- package/src/studio/samples/index.ts +55 -0
- package/src/studio/samples/insurance.ts +195 -0
- package/src/studio/samples/inventory.ts +182 -0
- package/src/studio/samples/invoicing.ts +162 -0
- package/src/studio/samples/library.ts +180 -0
- package/src/studio/samples/live-data.test.ts +98 -0
- package/src/studio/samples/live-data.ts +308 -0
- package/src/studio/samples/projects.ts +190 -0
- package/src/studio/samples/realestate.ts +196 -0
- package/src/studio/samples/restaurant.ts +187 -0
- package/src/studio/samples/samples.test.ts +208 -0
- package/src/studio/samples/school.ts +197 -0
- package/src/studio/samples/seed-floor.test.ts +25 -0
- package/src/studio/samples/shared.ts +305 -0
- package/src/studio/samples/subscriptions.ts +183 -0
- package/src/studio/samples/support.ts +182 -0
- package/src/studio/scaffold-app.test.ts +91 -0
- package/src/studio/scaffold-app.ts +161 -0
- package/src/studio/scaffold.test.ts +178 -0
- package/src/studio/scaffold.ts +374 -0
- package/src/studio/themes.ts +172 -0
- package/src/studio/verify.test.ts +47 -0
- package/src/studio/verify.ts +79 -0
- package/src/sveltekit/in-memory.test.ts +104 -0
- package/src/sveltekit/in-memory.ts +129 -0
- package/src/sveltekit/index.ts +23 -0
- package/src/sveltekit/query-plan.test.ts +109 -0
- package/src/sveltekit/query-plan.ts +125 -0
- package/src/sveltekit/sql-source.test.ts +185 -0
- package/src/sveltekit/sql-source.ts +166 -0
- package/src/sveltekit/sql.test.ts +80 -0
- package/src/sveltekit/sql.ts +131 -0
- package/src/sveltekit/transport.test.ts +175 -0
- package/src/sveltekit/transport.ts +254 -0
- package/src/sveltekit/types.ts +11 -0
- package/src/upgrade-prompt.ts +149 -148
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { EntitySchema } from '../../schema.js'
|
|
2
|
+
import type { FormatRule } from '../project.js'
|
|
3
|
+
import { screen, formScreen, project, dashScreen, statusPills, detailScreen, pad, ids, type SampleApp } from './shared.js'
|
|
4
|
+
|
|
5
|
+
const books: EntitySchema = {
|
|
6
|
+
name: 'books',
|
|
7
|
+
label: 'Book',
|
|
8
|
+
idField: 'id',
|
|
9
|
+
fields: [
|
|
10
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
11
|
+
{ field: 'title', type: 'text', required: true, minLength: 2 },
|
|
12
|
+
{ field: 'author', type: 'text', label: 'Author', required: true, minLength: 2 },
|
|
13
|
+
{ field: 'isbn', type: 'text', label: 'ISBN', input: { editorType: 'mask', mask: '###-#-#####-###-#', help: 'ISBN-13, digits only' } },
|
|
14
|
+
{ field: 'genre', type: 'enum', required: true, defaultValue: 'fiction', options: [
|
|
15
|
+
{ value: 'fiction', label: 'Fiction', color: '#6366f1' },
|
|
16
|
+
{ value: 'science', label: 'Science', color: '#0ea5e9' },
|
|
17
|
+
{ value: 'history', label: 'History', color: '#f59e0b' },
|
|
18
|
+
{ value: 'kids', label: 'Kids', color: '#ec4899' },
|
|
19
|
+
] },
|
|
20
|
+
{ field: 'rating', type: 'number', label: 'Rating', min: 0, max: 5, defaultValue: 3, input: { editorType: 'rating', help: 'Reader score 0-5' } },
|
|
21
|
+
{ field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Themes / keywords' } },
|
|
22
|
+
{ field: 'spineColor', type: 'text', label: 'Spine color', defaultValue: '#6366f1', input: { editorType: 'color', help: 'Shelf label color' } },
|
|
23
|
+
{ field: 'copies', type: 'number', label: 'Copies', min: 0, defaultValue: 1, input: { help: 'Total copies owned' } },
|
|
24
|
+
{ field: 'year', type: 'number', label: 'Year', min: 0 },
|
|
25
|
+
{ field: 'coverUrl', type: 'text', label: 'Cover', format: 'url', input: { placeholder: 'https://…' } },
|
|
26
|
+
],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const members: EntitySchema = {
|
|
30
|
+
name: 'members',
|
|
31
|
+
label: 'Member',
|
|
32
|
+
idField: 'id',
|
|
33
|
+
fields: [
|
|
34
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
35
|
+
{ field: 'name', type: 'text', required: true, minLength: 2 },
|
|
36
|
+
{ field: 'email', type: 'text', format: 'email', required: true, input: { help: 'Used for hold notices' } },
|
|
37
|
+
{ field: 'phone', type: 'text', label: 'Phone', input: { editorType: 'phone' } },
|
|
38
|
+
{ field: 'country', type: 'text', label: 'Country', input: { editorType: 'country', help: 'Residence country' } },
|
|
39
|
+
{ field: 'tier', type: 'enum', required: true, defaultValue: 'standard', options: [
|
|
40
|
+
{ value: 'standard', label: 'Standard', color: '#6366f1' },
|
|
41
|
+
{ value: 'student', label: 'Student', color: '#10b981' },
|
|
42
|
+
{ value: 'senior', label: 'Senior', color: '#f59e0b' },
|
|
43
|
+
] },
|
|
44
|
+
{ field: 'pin', type: 'text', label: 'Card PIN', minLength: 4, hidden: { grid: true }, input: { editorType: 'password', help: 'Self-checkout PIN' } },
|
|
45
|
+
{ field: 'joinDate', type: 'date', label: 'Joined' },
|
|
46
|
+
{ field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const loans: EntitySchema = {
|
|
51
|
+
name: 'loans',
|
|
52
|
+
label: 'Loan',
|
|
53
|
+
idField: 'id',
|
|
54
|
+
fields: [
|
|
55
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
56
|
+
{ field: 'bookId', type: 'relation', label: 'Book', relation: { entity: 'books', foreignKey: 'bookId', labelField: 'title' } },
|
|
57
|
+
{ field: 'memberId', type: 'relation', label: 'Member', relation: { entity: 'members', foreignKey: 'memberId', labelField: 'name' } },
|
|
58
|
+
{ field: 'status', type: 'enum', required: true, defaultValue: 'borrowed', options: [
|
|
59
|
+
{ value: 'borrowed', label: 'Borrowed', color: '#0ea5e9' },
|
|
60
|
+
{ value: 'returned', label: 'Returned', color: '#10b981' },
|
|
61
|
+
{ value: 'overdue', label: 'Overdue', color: '#ef4444' },
|
|
62
|
+
] },
|
|
63
|
+
{ field: 'borrowedAt', type: 'date', label: 'Borrowed' },
|
|
64
|
+
{ field: 'dueDate', type: 'date', label: 'Due date' },
|
|
65
|
+
{ field: 'returnedAt', type: 'text', label: 'Returned at', input: { editorType: 'datetime', help: 'Exact check-in time' } },
|
|
66
|
+
{ field: 'daysOverdue', type: 'number', label: 'Days overdue', min: 0, defaultValue: 0, input: { help: 'Days past the due date' } },
|
|
67
|
+
{ field: 'dailyRate', type: 'number', label: 'Daily rate ($)', min: 0, defaultValue: 0.25, input: { help: 'Fine charged per overdue day' } },
|
|
68
|
+
// Derived: total fine owed = overdue days times the daily rate (display-only).
|
|
69
|
+
{ field: 'fineAmount', type: 'number', label: 'Fine ($)', readonly: true, formula: 'daysOverdue * dailyRate' },
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const seed = {
|
|
74
|
+
books: [
|
|
75
|
+
{ id: 'bk1', title: 'The Silent Sea', author: 'Marina Cole', isbn: '978-0-14028-329-5', genre: 'fiction', rating: 4, tags: ['Mystery', 'Ocean'], spineColor: '#6366f1', copies: 4, year: 2019, coverUrl: 'https://covers.example.com/silent-sea.jpg' },
|
|
76
|
+
{ id: 'bk2', title: 'Quantum Frontiers', author: 'Devan Rao', isbn: '978-1-40889-641-3', genre: 'science', rating: 5, tags: ['Physics', 'Space'], spineColor: '#0ea5e9', copies: 3, year: 2021, coverUrl: 'https://covers.example.com/quantum.jpg' },
|
|
77
|
+
{ id: 'bk3', title: 'Empires of Old', author: 'Sofia Marek', isbn: '978-0-33450-403-8', genre: 'history', rating: 3, tags: ['Rome', 'War'], spineColor: '#f59e0b', copies: 2, year: 2015, coverUrl: 'https://covers.example.com/empires.jpg' },
|
|
78
|
+
{ id: 'bk4', title: 'The Curious Fox', author: 'Penny Hart', isbn: '978-1-59327-599-0', genre: 'kids', rating: 5, tags: ['Animals', 'Adventure'], spineColor: '#ec4899', copies: 6, year: 2022, coverUrl: 'https://covers.example.com/fox.jpg' },
|
|
79
|
+
{ id: 'bk5', title: 'Midnight Harbor', author: 'Louis Brenn', isbn: '978-0-74322-620-9', genre: 'fiction', rating: 4, tags: ['Noir', 'Thriller'], spineColor: '#8b5cf6', copies: 5, year: 2018, coverUrl: 'https://covers.example.com/harbor.jpg' },
|
|
80
|
+
{ id: 'bk6', title: 'Atoms and Stars', author: 'Iris Chen', isbn: '978-0-30788-743-6', genre: 'science', rating: 4, tags: ['Astronomy', 'Chemistry'], spineColor: '#14b8a6', copies: 3, year: 2020, coverUrl: 'https://covers.example.com/atoms.jpg' },
|
|
81
|
+
],
|
|
82
|
+
members: [
|
|
83
|
+
{ id: 'mb1', name: 'Eleanor Price', email: 'eleanor.price@example.com', phone: '+1 (415) 555-0111', country: 'United States', tier: 'standard', pin: '4821', joinDate: '2023-02-14', active: true },
|
|
84
|
+
{ id: 'mb2', name: 'Ravi Menon', email: 'ravi.menon@example.com', phone: '+44 20 7946 0112', country: 'United Kingdom', tier: 'student', pin: '9037', joinDate: '2024-09-01', active: true },
|
|
85
|
+
{ id: 'mb3', name: 'Greta Olsen', email: 'greta.olsen@example.com', phone: '+49 30 5550113', country: 'Germany', tier: 'senior', pin: '2264', joinDate: '2021-06-30', active: true },
|
|
86
|
+
{ id: 'mb4', name: 'Tomas Vega', email: 'tomas.vega@example.com', phone: '+34 91 555 0114', country: 'Spain', tier: 'standard', pin: '7715', joinDate: '2022-11-19', active: true },
|
|
87
|
+
{ id: 'mb5', name: 'Aisha Bello', email: 'aisha.bello@example.com', phone: '+234 1 555 0115', country: 'Nigeria', tier: 'student', pin: '3390', joinDate: '2025-01-08', active: true },
|
|
88
|
+
{ id: 'mb6', name: 'Harold Finch', email: 'harold.finch@example.com', phone: '+1 (212) 555-0116', country: 'United States', tier: 'senior', pin: '5582', joinDate: '2020-03-27', active: false },
|
|
89
|
+
],
|
|
90
|
+
loans: [
|
|
91
|
+
{ id: 'ln1', bookId: 'bk1', memberId: 'mb1', status: 'borrowed', borrowedAt: '2026-07-14', dueDate: '2026-07-28', returnedAt: '', daysOverdue: 0, dailyRate: 0.25 },
|
|
92
|
+
{ id: 'ln2', bookId: 'bk2', memberId: 'mb2', status: 'overdue', borrowedAt: '2026-06-21', dueDate: '2026-07-05', returnedAt: '', daysOverdue: 18, dailyRate: 0.25 },
|
|
93
|
+
{ id: 'ln3', bookId: 'bk3', memberId: 'mb3', status: 'returned', borrowedAt: '2026-06-06', dueDate: '2026-06-20', returnedAt: '2026-06-19T15:42:00.000Z', daysOverdue: 0, dailyRate: 0.25 },
|
|
94
|
+
{ id: 'ln4', bookId: 'bk4', memberId: 'mb4', status: 'borrowed', borrowedAt: '2026-07-19', dueDate: '2026-08-02', returnedAt: '', daysOverdue: 0, dailyRate: 0.10 },
|
|
95
|
+
{ id: 'ln5', bookId: 'bk5', memberId: 'mb5', status: 'overdue', borrowedAt: '2026-06-16', dueDate: '2026-06-30', returnedAt: '', daysOverdue: 29, dailyRate: 0.25 },
|
|
96
|
+
{ id: 'ln6', bookId: 'bk1', memberId: 'mb6', status: 'returned', borrowedAt: '2026-06-01', dueDate: '2026-06-15', returnedAt: '2026-06-18T09:05:00.000Z', daysOverdue: 3, dailyRate: 0.25 },
|
|
97
|
+
{ id: 'ln7', bookId: 'bk6', memberId: 'mb2', status: 'borrowed', borrowedAt: '2026-07-27', dueDate: '2026-08-10', returnedAt: '', daysOverdue: 0, dailyRate: 0.25 },
|
|
98
|
+
{ id: 'ln8', bookId: 'bk2', memberId: 'mb3', status: 'overdue', borrowedAt: '2026-06-17', dueDate: '2026-07-01', returnedAt: '', daysOverdue: 24, dailyRate: 0.50 },
|
|
99
|
+
],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Loan grid formatting: status pills, plus numeric thresholds so overdue loans
|
|
103
|
+
// jump out (any overdue day red + bold, a badly overdue loan gets a red wash) -
|
|
104
|
+
// data-driven color, not just status pills.
|
|
105
|
+
const loanFormats: FormatRule[] = [
|
|
106
|
+
...statusPills(loans, 'status'),
|
|
107
|
+
{ field: 'daysOverdue', op: 'gt', value: 0, color: '#dc2626', bold: true },
|
|
108
|
+
{ field: 'daysOverdue', op: 'gte', value: 21, background: 'color-mix(in srgb, #ef4444 18%, transparent)' },
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
// Book grid formatting: genre pills plus rating thresholds (top-rated green +
|
|
112
|
+
// bold, weak titles red).
|
|
113
|
+
const bookFormats: FormatRule[] = [
|
|
114
|
+
...statusPills(books, 'genre'),
|
|
115
|
+
{ field: 'rating', op: 'gte', value: 4, color: '#16a34a', bold: true },
|
|
116
|
+
{ field: 'rating', op: 'lt', value: 3, color: '#dc2626' },
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
export const library: SampleApp = {
|
|
120
|
+
id: 'library',
|
|
121
|
+
name: 'Library',
|
|
122
|
+
description: 'Books, members and loans - circulation + catalog dashboards (an overdue-days KPI sparkline over borrow dates, a rating gauge, status and genre breakdowns, a status x borrow-date pivot, tabbed views), conditional-format status-pill grids with overdue-red rows and row actions, master/detail, and rich edit forms (ISBN mask, star rating, tag chips, phone, country, and a computed fine).',
|
|
123
|
+
emoji: '📚',
|
|
124
|
+
accent: '#a855f7',
|
|
125
|
+
build: () => {
|
|
126
|
+
const bookRows = pad(books, seed.books, 60)
|
|
127
|
+
const memberRows = pad(members, seed.members, 40)
|
|
128
|
+
const loanRows = pad(loans, seed.loans, 90, { bookId: ids(bookRows), memberId: ids(memberRows) })
|
|
129
|
+
return project({
|
|
130
|
+
title: 'BookNook',
|
|
131
|
+
brand: 'BookNook',
|
|
132
|
+
accent: '#a855f7',
|
|
133
|
+
preset: 'shadcn',
|
|
134
|
+
footer: '© BookNook Library',
|
|
135
|
+
entities: [books, members, loans],
|
|
136
|
+
seed: { books: bookRows, members: memberRows, loans: loanRows },
|
|
137
|
+
screens: [
|
|
138
|
+
// Circulation dashboard: loan KPIs (overdue-days card trends over borrow
|
|
139
|
+
// dates as a sparkline), an average-overdue gauge, status breakdowns, a
|
|
140
|
+
// status x borrow-date pivot, a tabbed view, and a status-pill loan grid
|
|
141
|
+
// where overdue rows go red.
|
|
142
|
+
dashScreen(loans, { id: 'overview', title: 'Circulation', order: 0 }, [
|
|
143
|
+
{ kpi: 'Active loans', reduce: 'count', span: 1 },
|
|
144
|
+
{ kpi: 'Days overdue', measure: 'daysOverdue', reduce: 'sum', trendField: 'borrowedAt', trendReduce: 'sum', span: 1 },
|
|
145
|
+
{ kpi: 'Avg daily rate', measure: 'dailyRate', reduce: 'avg', format: 'currency', span: 1 },
|
|
146
|
+
{ gauge: 'Avg days overdue', measure: 'daysOverdue', reduce: 'avg', min: 0, max: 30, unit: 'd', span: 1 },
|
|
147
|
+
{ chart: 'status', measure: 'daysOverdue', reduce: 'sum', type: 'bar', span: 2 },
|
|
148
|
+
{ chart: 'status', reduce: 'count', type: 'pie', span: 1 },
|
|
149
|
+
{ pivot: { rows: ['status'], cols: ['borrowedAt'], measure: 'daysOverdue', aggregate: 'sum' }, span: 3 },
|
|
150
|
+
{ tabs: [
|
|
151
|
+
{ label: 'Overdue by status', tiles: [{ chart: 'status', measure: 'daysOverdue', reduce: 'sum', type: 'bar' }] },
|
|
152
|
+
{ label: 'Loans by status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
|
|
153
|
+
], span: 3 },
|
|
154
|
+
{ grid: true, format: loanFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
|
|
155
|
+
]),
|
|
156
|
+
// Catalog dashboard: book KPIs (avg rating targets 4 stars), a rating gauge
|
|
157
|
+
// 0-5, copies + count by genre, a copies-by-year area trend, a genre filter,
|
|
158
|
+
// and a genre-pill book grid with rating thresholds.
|
|
159
|
+
dashScreen(books, { id: 'catalog', title: 'Catalog', order: 1 }, [
|
|
160
|
+
{ kpi: 'Titles', reduce: 'count', span: 1 },
|
|
161
|
+
{ kpi: 'Copies on shelf', measure: 'copies', reduce: 'sum', format: 'compact', span: 1 },
|
|
162
|
+
{ kpi: 'Avg rating', measure: 'rating', reduce: 'avg', target: 4, span: 1 },
|
|
163
|
+
{ gauge: 'Avg rating', measure: 'rating', reduce: 'avg', min: 0, max: 5, span: 1 },
|
|
164
|
+
{ chart: 'genre', measure: 'copies', reduce: 'sum', type: 'bar', span: 2 },
|
|
165
|
+
{ chart: 'genre', reduce: 'count', type: 'pie', span: 1 },
|
|
166
|
+
{ chart: 'year', measure: 'copies', reduce: 'sum', type: 'area', span: 3 },
|
|
167
|
+
{ filter: ['genre'], span: 3 },
|
|
168
|
+
{ grid: true, format: bookFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
|
|
169
|
+
]),
|
|
170
|
+
detailScreen(members, { id: 'member-360', title: 'Member 360', order: 2 }, {
|
|
171
|
+
titleField: 'name', subtitleField: 'email', statusField: 'tier',
|
|
172
|
+
related: [{ entity: 'loans', foreignKey: 'memberId', label: 'Loans', titleField: 'bookId', dateField: 'borrowedAt', statusField: 'status' }],
|
|
173
|
+
}),
|
|
174
|
+
screen(books, 'master-detail', { id: 'books', title: 'Books', order: 3, child: loans, foreignKey: 'bookId' }),
|
|
175
|
+
formScreen(loans, { id: 'loans', title: 'Loans', order: 4 }, undefined, { format: loanFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['status', 'bookId', 'memberId']),
|
|
176
|
+
formScreen(members, { id: 'members', title: 'Members', order: 5 }, undefined, { rowLink: { screen: 'member-360', sourceField: 'id', targetField: 'id' } }, ['tier', 'country', 'active']),
|
|
177
|
+
],
|
|
178
|
+
})
|
|
179
|
+
},
|
|
180
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { compile } from 'svelte/compiler'
|
|
3
|
+
import { liveDataSamples, getLiveDataSample } from './live-data.js'
|
|
4
|
+
import { validateProject, parseProject, serializeProject, type EntityDataSource } from '../project.js'
|
|
5
|
+
import { emitStudioProject } from '../emit-project.js'
|
|
6
|
+
|
|
7
|
+
describe('live-data starters', () => {
|
|
8
|
+
it('ships a gallery with unique ids and card metadata', () => {
|
|
9
|
+
expect(liveDataSamples.length).toBeGreaterThanOrEqual(3)
|
|
10
|
+
const ids = liveDataSamples.map((s) => s.id)
|
|
11
|
+
expect(new Set(ids).size).toBe(ids.length)
|
|
12
|
+
for (const s of liveDataSamples) {
|
|
13
|
+
expect(s.name).toBeTruthy()
|
|
14
|
+
expect(s.description).toBeTruthy()
|
|
15
|
+
expect(s.emoji).toBeTruthy()
|
|
16
|
+
expect(s.accent).toMatch(/^#[0-9a-f]{6}$/i)
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('getLiveDataSample resolves by id', () => {
|
|
21
|
+
expect(getLiveDataSample('live-northwind-pglite')?.name).toBe('Northwind · Local Postgres')
|
|
22
|
+
expect(getLiveDataSample('nope')).toBeUndefined()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
for (const sample of liveDataSamples) {
|
|
26
|
+
describe(sample.id, () => {
|
|
27
|
+
const project = sample.build()
|
|
28
|
+
|
|
29
|
+
it('is a valid project (no errors)', () => {
|
|
30
|
+
const errors = validateProject(project).filter((i) => i.level === 'error')
|
|
31
|
+
expect(errors, JSON.stringify(errors)).toHaveLength(0)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('binds every entity to a REAL source (never memory)', () => {
|
|
35
|
+
expect(project.entities.length).toBeGreaterThanOrEqual(1)
|
|
36
|
+
for (const e of project.entities) {
|
|
37
|
+
const src = project.dataSources?.[e.name] as EntityDataSource | undefined
|
|
38
|
+
expect(src, `${e.name}: no source`).toBeTruthy()
|
|
39
|
+
expect(src!.kind).not.toBe('memory')
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('round-trips through parse/serialize', () => {
|
|
44
|
+
const again = parseProject(serializeProject(project))
|
|
45
|
+
expect(again.entities.map((e) => e.name)).toEqual(project.entities.map((e) => e.name))
|
|
46
|
+
expect(again.dataSources).toEqual(project.dataSources)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('generates compiling Svelte pages', () => {
|
|
50
|
+
const files = emitStudioProject(project)
|
|
51
|
+
for (const f of files.filter((f) => f.path.endsWith('.svelte'))) {
|
|
52
|
+
expect(() => compile(f.contents, { filename: f.path, generate: 'client' }), f.path).not.toThrow()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
it('every starter is a real app: a dashboard, a detail page, and drill-through', () => {
|
|
59
|
+
for (const s of liveDataSamples) {
|
|
60
|
+
const blocks = s.build().screens.flatMap((sc) => sc.blocks)
|
|
61
|
+
expect(blocks.some((b) => b.config.kind === 'kpi'), `${s.id}: no dashboard KPIs`).toBe(true)
|
|
62
|
+
const all = emitStudioProject(s.build()).map((f) => f.contents).join('\n')
|
|
63
|
+
expect(all.includes('SvRecordDetail'), `${s.id}: no detail page`).toBe(true)
|
|
64
|
+
expect(all.includes('import { goto }'), `${s.id}: no drill-through`).toBe(true)
|
|
65
|
+
expect(all.includes("selectedId={$page.url.searchParams.get('id')"), `${s.id}: detail not URL-addressable`).toBe(true)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('the Northwind (Supabase) detail timelines match the denormalized view by name/company', () => {
|
|
70
|
+
const all = emitStudioProject(getLiveDataSample('live-northwind-supabase')!.build()).map((f) => f.contents).join('\n')
|
|
71
|
+
expect(all).toContain('parentField: "name"') // product -> order_lines by product name
|
|
72
|
+
expect(all).toContain('parentField: "company"') // customer -> order_lines by company
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('PGlite starter generates embedded Postgres bootstrap + seeded rows', () => {
|
|
76
|
+
const data = emitStudioProject(getLiveDataSample('live-northwind-pglite')!.build()).find((f) => f.path.endsWith('src/lib/data.ts'))!.contents
|
|
77
|
+
expect(data).toContain("import { PGlite } from '@electric-sql/pglite'")
|
|
78
|
+
expect(data).toContain('createSqlDataSource')
|
|
79
|
+
expect(data).toContain('Chai') // a curated seed row is baked in
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('Supabase starter generates a client bound to the hosted sample', () => {
|
|
83
|
+
const files = emitStudioProject(getLiveDataSample('live-northwind-supabase')!.build())
|
|
84
|
+
const conn = files.find((f) => f.path.endsWith('src/lib/connections.ts'))!.contents
|
|
85
|
+
expect(conn).toContain('rbnnlzgtfzsylllniozo.supabase.co')
|
|
86
|
+
expect(conn).toContain('createClient')
|
|
87
|
+
const data = files.find((f) => f.path.endsWith('src/lib/data.ts'))!.contents
|
|
88
|
+
expect(data).toContain('createSupabaseDataSource')
|
|
89
|
+
expect(data).toContain("table: 'order_lines'")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('REST starter generates createRestDataSource against DummyJSON', () => {
|
|
93
|
+
const data = emitStudioProject(getLiveDataSample('live-dummyjson-rest')!.build()).find((f) => f.path.endsWith('src/lib/data.ts'))!.contents
|
|
94
|
+
expect(data).toContain('createRestDataSource')
|
|
95
|
+
expect(data).toContain('dummyjson.com/products')
|
|
96
|
+
expect(data).toContain('body?.products') // rowsPath parse
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +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?: 'sidebar' | 'top-nav'
|
|
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
|
+
}
|