@entrinsik/vite-plugin-informer 1.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.
@@ -0,0 +1,242 @@
1
+ # API Quick Reference
2
+
3
+ All endpoints are relative to `/api`. The Vite plugin handles authentication in dev mode.
4
+
5
+ ## Datasets
6
+
7
+ ### GET /api/datasets-list
8
+ List all datasets the user can access.
9
+
10
+ ```javascript
11
+ const datasets = await fetch('/api/datasets-list').then(r => r.json());
12
+ // [{ id, name, description, records, size, tags, sharing }, ...]
13
+ ```
14
+
15
+ ### POST /api/datasets/{id}/_search
16
+ Query dataset data using Elasticsearch DSL.
17
+
18
+ ```javascript
19
+ const result = await fetch(`/api/datasets/${id}/_search`, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify({
23
+ query: { match_all: {} },
24
+ size: 100,
25
+ from: 0,
26
+ _source: ['field1', 'field2'],
27
+ sort: [{ field1: 'desc' }],
28
+ aggs: { total: { sum: { field: 'amount' } } }
29
+ })
30
+ }).then(r => r.json());
31
+
32
+ // result.hits.total - count
33
+ // result.hits.hits - [{ _source: { ... } }, ...]
34
+ // result.aggregations - { total: { value: 12345 } }
35
+ ```
36
+
37
+ **Query patterns:**
38
+ ```javascript
39
+ // All records
40
+ { query: { match_all: {} } }
41
+
42
+ // Exact match
43
+ { query: { bool: { filter: [{ term: { status: 'active' } }] } } }
44
+
45
+ // Range
46
+ { query: { bool: { filter: [{ range: { amount: { gte: 1000 } } }] } } }
47
+
48
+ // Date range
49
+ { query: { bool: { filter: [{ range: { date: { gte: '2024-01-01', lte: '2024-12-31' } } }] } } }
50
+
51
+ // Multiple filters
52
+ { query: { bool: { filter: [
53
+ { term: { region: 'North' } },
54
+ { range: { amount: { gte: 1000 } } }
55
+ ] } } }
56
+
57
+ // Text search
58
+ { query: { bool: { must: [{ match: { name: 'search text' } }] } } }
59
+ ```
60
+
61
+ **Aggregation patterns:**
62
+ ```javascript
63
+ // Metrics
64
+ { aggs: { total: { sum: { field: 'amount' } } } }
65
+ { aggs: { average: { avg: { field: 'amount' } } } }
66
+ { aggs: { minimum: { min: { field: 'amount' } } } }
67
+ { aggs: { maximum: { max: { field: 'amount' } } } }
68
+ { aggs: { count: { value_count: { field: 'id' } } } }
69
+ { aggs: { unique: { cardinality: { field: 'customer' } } } }
70
+
71
+ // Group by
72
+ { aggs: { by_status: { terms: { field: 'status', size: 50 } } } }
73
+
74
+ // Group with metric
75
+ { aggs: {
76
+ by_region: {
77
+ terms: { field: 'region', size: 50 },
78
+ aggs: { total: { sum: { field: 'amount' } } }
79
+ }
80
+ } }
81
+
82
+ // Date histogram
83
+ { aggs: {
84
+ by_month: {
85
+ date_histogram: { field: 'date', calendar_interval: 'month' },
86
+ aggs: { total: { sum: { field: 'amount' } } }
87
+ }
88
+ } }
89
+ ```
90
+
91
+ ## Queries
92
+
93
+ ### GET /api/queries-list
94
+ List saved queries.
95
+
96
+ ```javascript
97
+ const queries = await fetch('/api/queries-list').then(r => r.json());
98
+ // [{ id, name, description, tags, sharing }, ...]
99
+ ```
100
+
101
+ ### POST /api/queries/{id}/_execute
102
+ Execute a saved query.
103
+
104
+ ```javascript
105
+ const result = await fetch(`/api/queries/${id}/_execute`, {
106
+ method: 'POST',
107
+ headers: { 'Content-Type': 'application/json' },
108
+ body: JSON.stringify({
109
+ parameters: { startDate: '2024-01-01' }
110
+ })
111
+ }).then(r => r.json());
112
+ ```
113
+
114
+ ## Integrations
115
+
116
+ ### GET /api/integrations
117
+ List configured integrations.
118
+
119
+ ```javascript
120
+ const result = await fetch('/api/integrations').then(r => r.json());
121
+ // result.items = [{ id, name, slug, type, description }, ...]
122
+ ```
123
+
124
+ ### POST /api/integrations/{id}/request
125
+ Make an authenticated request through an integration.
126
+
127
+ ```javascript
128
+ const result = await fetch(`/api/integrations/${slugOrId}/request`, {
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/json' },
131
+ body: JSON.stringify({
132
+ url: '/path/to/endpoint',
133
+ method: 'GET', // GET, POST, PUT, PATCH, DELETE
134
+ params: { key: 'value' }, // Query parameters
135
+ data: { key: 'value' }, // Request body (POST/PUT/PATCH)
136
+ headers: {} // Additional headers
137
+ })
138
+ }).then(r => r.json());
139
+
140
+ // result.status - HTTP status
141
+ // result.data - Response body
142
+ // result.error - true if error status
143
+ ```
144
+
145
+ **Salesforce examples:**
146
+
147
+ ```javascript
148
+ // SOQL query
149
+ await fetch('/api/integrations/salesforce/request', {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ body: JSON.stringify({
153
+ url: '/data/v59.0/query',
154
+ method: 'GET',
155
+ params: { q: "SELECT Id, Name FROM Account LIMIT 10" }
156
+ })
157
+ });
158
+
159
+ // Get record
160
+ await fetch('/api/integrations/salesforce/request', {
161
+ method: 'POST',
162
+ headers: { 'Content-Type': 'application/json' },
163
+ body: JSON.stringify({
164
+ url: '/data/v59.0/sobjects/Account/001xxxxxxxxxxxx',
165
+ method: 'GET'
166
+ })
167
+ });
168
+
169
+ // Create record
170
+ await fetch('/api/integrations/salesforce/request', {
171
+ method: 'POST',
172
+ headers: { 'Content-Type': 'application/json' },
173
+ body: JSON.stringify({
174
+ url: '/data/v59.0/sobjects/Contact',
175
+ method: 'POST',
176
+ data: { FirstName: 'John', LastName: 'Doe', Email: 'john@example.com' }
177
+ })
178
+ });
179
+ ```
180
+
181
+ ## Data Access Configuration
182
+
183
+ Create `data-access.yaml` in your project root to declare which APIs your report needs. Without this file, all API access is blocked when published.
184
+
185
+ ```yaml
186
+ # data-access.yaml
187
+
188
+ datasets:
189
+ - admin:sales-data
190
+
191
+ queries:
192
+ - admin:summary
193
+
194
+ integrations:
195
+ - salesforce
196
+ ```
197
+
198
+ ### Row-Level Security
199
+
200
+ ```yaml
201
+ datasets:
202
+ - id: admin:orders
203
+ filter:
204
+ region: $user.custom.region
205
+ owner: $user.username
206
+ ```
207
+
208
+ ### Integration Credentials
209
+
210
+ ```yaml
211
+ integrations:
212
+ - id: partner-api
213
+ headers:
214
+ Authorization: Bearer $user.custom.apiToken
215
+ params:
216
+ tenant: $tenant.id
217
+ ```
218
+
219
+ ### Variables
220
+
221
+ - `$user.username`, `$user.email`, `$user.displayName`
222
+ - `$user.custom.xxx` - Custom user fields
223
+ - `$tenant.id`
224
+ - `$report.id`, `$report.name`
225
+
226
+ ## Error Handling
227
+
228
+ API errors return:
229
+ ```javascript
230
+ {
231
+ statusCode: 400,
232
+ error: 'Bad Request',
233
+ message: 'Description of error'
234
+ }
235
+ ```
236
+
237
+ Common status codes:
238
+ - `400` - Bad request / validation error
239
+ - `401` - Not authenticated
240
+ - `403` - Not authorized
241
+ - `404` - Not found
242
+ - `502` - Upstream error (integration request failed)
@@ -0,0 +1,299 @@
1
+ # Report Templates
2
+
3
+ ## Minimal Starter
4
+
5
+ ### index.html
6
+ ```html
7
+ <!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8">
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
+ <title>Report</title>
13
+ <link rel="stylesheet" href="styles.css">
14
+ </head>
15
+ <body>
16
+ <div id="app">
17
+ <h1>Report Title</h1>
18
+ <div id="content"></div>
19
+ </div>
20
+ <script type="module" src="main.js"></script>
21
+ </body>
22
+ </html>
23
+ ```
24
+
25
+ ### styles.css
26
+ ```css
27
+ * { box-sizing: border-box; margin: 0; padding: 0; }
28
+
29
+ body {
30
+ font-family: system-ui, sans-serif;
31
+ background: #0f172a;
32
+ color: #f1f5f9;
33
+ padding: 24px;
34
+ }
35
+
36
+ #app {
37
+ max-width: 1200px;
38
+ margin: 0 auto;
39
+ }
40
+
41
+ h1 {
42
+ font-size: 24px;
43
+ margin-bottom: 24px;
44
+ }
45
+ ```
46
+
47
+ ### main.js
48
+ ```javascript
49
+ window.informerReady = false;
50
+
51
+ async function init() {
52
+ // List available datasets
53
+ const datasets = await fetch('/api/datasets-list').then(r => r.json());
54
+ console.log('Available datasets:', datasets);
55
+
56
+ // Query a dataset
57
+ if (datasets.length > 0) {
58
+ const result = await fetch(`/api/datasets/${datasets[0].id}/_search`, {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify({ query: { match_all: {} }, size: 10 })
62
+ }).then(r => r.json());
63
+
64
+ const records = result.hits.hits.map(h => h._source);
65
+ console.log('Records:', records);
66
+
67
+ // Render your content here
68
+ document.getElementById('content').innerHTML = `
69
+ <p>Found ${result.hits.total} records</p>
70
+ <pre>${JSON.stringify(records, null, 2)}</pre>
71
+ `;
72
+ }
73
+
74
+ window.informerReady = true;
75
+ }
76
+
77
+ init();
78
+ ```
79
+
80
+ ## Dashboard Layout
81
+
82
+ ### index.html
83
+ ```html
84
+ <!DOCTYPE html>
85
+ <html lang="en">
86
+ <head>
87
+ <meta charset="UTF-8">
88
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
89
+ <title>Dashboard</title>
90
+ <link rel="stylesheet" href="styles.css">
91
+ </head>
92
+ <body>
93
+ <div class="dashboard">
94
+ <header>
95
+ <h1>Dashboard Title</h1>
96
+ <p class="subtitle">Description of this dashboard</p>
97
+ </header>
98
+
99
+ <div class="metrics">
100
+ <div class="metric-card">
101
+ <span class="metric-value" id="metric1">--</span>
102
+ <span class="metric-label">Metric 1</span>
103
+ </div>
104
+ <div class="metric-card">
105
+ <span class="metric-value" id="metric2">--</span>
106
+ <span class="metric-label">Metric 2</span>
107
+ </div>
108
+ <div class="metric-card">
109
+ <span class="metric-value" id="metric3">--</span>
110
+ <span class="metric-label">Metric 3</span>
111
+ </div>
112
+ <div class="metric-card">
113
+ <span class="metric-value" id="metric4">--</span>
114
+ <span class="metric-label">Metric 4</span>
115
+ </div>
116
+ </div>
117
+
118
+ <div class="content">
119
+ <!-- Add your charts, tables, or other visualizations here -->
120
+ <div id="visualization"></div>
121
+ </div>
122
+ </div>
123
+
124
+ <script type="module" src="main.js"></script>
125
+ </body>
126
+ </html>
127
+ ```
128
+
129
+ ### styles.css
130
+ ```css
131
+ * { box-sizing: border-box; margin: 0; padding: 0; }
132
+
133
+ :root {
134
+ --bg: #0f172a;
135
+ --bg-card: #1e293b;
136
+ --border: #334155;
137
+ --text: #f1f5f9;
138
+ --text-muted: #94a3b8;
139
+ --primary: #6366f1;
140
+ }
141
+
142
+ body {
143
+ font-family: system-ui, sans-serif;
144
+ background: var(--bg);
145
+ color: var(--text);
146
+ line-height: 1.5;
147
+ }
148
+
149
+ .dashboard {
150
+ max-width: 1400px;
151
+ margin: 0 auto;
152
+ padding: 32px 24px;
153
+ }
154
+
155
+ header {
156
+ margin-bottom: 32px;
157
+ }
158
+
159
+ header h1 {
160
+ font-size: 28px;
161
+ font-weight: 700;
162
+ }
163
+
164
+ .subtitle {
165
+ color: var(--text-muted);
166
+ margin-top: 4px;
167
+ }
168
+
169
+ .metrics {
170
+ display: grid;
171
+ grid-template-columns: repeat(4, 1fr);
172
+ gap: 16px;
173
+ margin-bottom: 24px;
174
+ }
175
+
176
+ .metric-card {
177
+ background: var(--bg-card);
178
+ border: 1px solid var(--border);
179
+ border-radius: 12px;
180
+ padding: 20px;
181
+ text-align: center;
182
+ }
183
+
184
+ .metric-value {
185
+ display: block;
186
+ font-size: 32px;
187
+ font-weight: 700;
188
+ color: var(--primary);
189
+ }
190
+
191
+ .metric-label {
192
+ display: block;
193
+ font-size: 13px;
194
+ color: var(--text-muted);
195
+ margin-top: 4px;
196
+ }
197
+
198
+ .content {
199
+ background: var(--bg-card);
200
+ border: 1px solid var(--border);
201
+ border-radius: 12px;
202
+ padding: 24px;
203
+ }
204
+
205
+ @media (max-width: 768px) {
206
+ .metrics { grid-template-columns: repeat(2, 1fr); }
207
+ }
208
+
209
+ @media (max-width: 480px) {
210
+ .metrics { grid-template-columns: 1fr; }
211
+ }
212
+ ```
213
+
214
+ ### main.js
215
+ ```javascript
216
+ window.informerReady = false;
217
+
218
+ async function init() {
219
+ try {
220
+ await loadData();
221
+ window.informerReady = true;
222
+ } catch (err) {
223
+ console.error('Failed to load:', err);
224
+ }
225
+ }
226
+
227
+ async function loadData() {
228
+ // Replace with your dataset ID
229
+ const datasetId = 'admin:your-dataset';
230
+
231
+ const result = await fetch(`/api/datasets/${datasetId}/_search`, {
232
+ method: 'POST',
233
+ headers: { 'Content-Type': 'application/json' },
234
+ body: JSON.stringify({
235
+ query: { match_all: {} },
236
+ size: 0,
237
+ aggs: {
238
+ total: { sum: { field: 'amount' } },
239
+ count: { value_count: { field: 'id' } },
240
+ avg: { avg: { field: 'amount' } },
241
+ by_category: {
242
+ terms: { field: 'category', size: 10 },
243
+ aggs: { total: { sum: { field: 'amount' } } }
244
+ }
245
+ }
246
+ })
247
+ }).then(r => r.json());
248
+
249
+ // Update metrics
250
+ const aggs = result.aggregations;
251
+ document.getElementById('metric1').textContent = formatCurrency(aggs.total.value);
252
+ document.getElementById('metric2').textContent = aggs.count.value.toLocaleString();
253
+ document.getElementById('metric3').textContent = formatCurrency(aggs.avg.value);
254
+ document.getElementById('metric4').textContent = result.hits.total.toLocaleString();
255
+
256
+ // Use aggs.by_category.buckets for visualization
257
+ // Each bucket has: { key: 'Category Name', doc_count: 123, total: { value: 456 } }
258
+ console.log('Category data:', aggs.by_category.buckets);
259
+ }
260
+
261
+ function formatCurrency(val) {
262
+ return new Intl.NumberFormat('en-US', {
263
+ style: 'currency',
264
+ currency: 'USD',
265
+ minimumFractionDigits: 0
266
+ }).format(val);
267
+ }
268
+
269
+ init();
270
+ ```
271
+
272
+ ## PDF Export Tips
273
+
274
+ The PDF renderer uses **print media** and adds a `.print` class to `<html>`.
275
+
276
+ ```css
277
+ /* Standard @media print works */
278
+ @media print {
279
+ body {
280
+ background: white;
281
+ color: black;
282
+ }
283
+
284
+ .no-print {
285
+ display: none;
286
+ }
287
+
288
+ /* Avoid page breaks inside charts/cards */
289
+ .chart-container,
290
+ .metric-card {
291
+ break-inside: avoid;
292
+ }
293
+ }
294
+
295
+ /* Or use the .print class */
296
+ .print .no-print {
297
+ display: none;
298
+ }
299
+ ```