@entrinsik/vite-plugin-informer 1.0.4 → 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/bin/deploy.js +2 -2
- package/bin/init.js +5 -38
- package/index.d.ts +2 -0
- package/package.json +21 -9
- package/src/client.js +6 -5
- package/src/deploy.js +69 -43
- package/templates/skill/SKILL.md +0 -436
- package/templates/skill/references/api-reference.md +0 -242
- package/templates/skill/references/report-templates.md +0 -299
package/bin/deploy.js
CHANGED
|
@@ -72,12 +72,12 @@ try {
|
|
|
72
72
|
id: savedId
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
// Save the
|
|
75
|
+
// Save the app ID back to package.json for future deploys
|
|
76
76
|
if (result.id && result.id !== savedId) {
|
|
77
77
|
if (!pkg.informer) pkg.informer = {};
|
|
78
78
|
pkg.informer.id = result.id;
|
|
79
79
|
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
80
|
-
console.log(`Saved
|
|
80
|
+
console.log(`Saved app ID "${result.id}" to package.json`);
|
|
81
81
|
}
|
|
82
82
|
} catch (err) {
|
|
83
83
|
console.error('Deploy failed:', err.message);
|
package/bin/init.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { readFile, writeFile, mkdir,
|
|
4
|
-
import { resolve,
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
|
|
4
|
+
import { resolve, basename } from 'node:path';
|
|
6
5
|
import { createInterface } from 'node:readline';
|
|
7
6
|
import { randomUUID } from 'node:crypto';
|
|
8
7
|
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
8
|
const cwd = process.cwd();
|
|
11
9
|
|
|
12
10
|
/**
|
|
@@ -279,10 +277,7 @@ INFORMER_API_KEY=your-api-key
|
|
|
279
277
|
console.log('Created data-access.yaml (configure API access for your report)');
|
|
280
278
|
}
|
|
281
279
|
|
|
282
|
-
// 10.
|
|
283
|
-
await copySkillFiles();
|
|
284
|
-
|
|
285
|
-
// 11. Add .env to .gitignore if not present
|
|
280
|
+
// 10. Add .env to .gitignore if not present
|
|
286
281
|
await updateGitignore();
|
|
287
282
|
|
|
288
283
|
console.log('\nSetup complete!\n');
|
|
@@ -291,8 +286,8 @@ INFORMER_API_KEY=your-api-key
|
|
|
291
286
|
console.log(' 2. Update data-access.yaml with the datasets/APIs your report needs');
|
|
292
287
|
console.log(' 3. Run: npm install');
|
|
293
288
|
console.log(' 4. Run: npm run dev');
|
|
294
|
-
console.log(' 5.
|
|
295
|
-
console.log('
|
|
289
|
+
console.log(' 5. Install the Claude skill: /plugin marketplace add entrinsik-org/claude-plugins');
|
|
290
|
+
console.log(' 6. Then: /plugin install magic-reports@entrinsik-plugins');
|
|
296
291
|
console.log('');
|
|
297
292
|
}
|
|
298
293
|
|
|
@@ -364,34 +359,6 @@ export default defineConfig({
|
|
|
364
359
|
console.log(`Updated ${configPath.split('/').pop()} with informer plugin`);
|
|
365
360
|
}
|
|
366
361
|
|
|
367
|
-
async function copySkillFiles() {
|
|
368
|
-
const skillSrc = resolve(__dirname, '..', 'templates', 'skill');
|
|
369
|
-
const skillDest = resolve(cwd, '.claude', 'skills', 'informer');
|
|
370
|
-
|
|
371
|
-
// Create destination directory
|
|
372
|
-
await mkdir(skillDest, { recursive: true });
|
|
373
|
-
|
|
374
|
-
// Copy files recursively
|
|
375
|
-
await copyDir(skillSrc, skillDest);
|
|
376
|
-
console.log('Copied Claude skill files to .claude/skills/informer/');
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
async function copyDir(src, dest) {
|
|
380
|
-
const entries = await readdir(src, { withFileTypes: true });
|
|
381
|
-
|
|
382
|
-
for (const entry of entries) {
|
|
383
|
-
const srcPath = join(src, entry.name);
|
|
384
|
-
const destPath = join(dest, entry.name);
|
|
385
|
-
|
|
386
|
-
if (entry.isDirectory()) {
|
|
387
|
-
await mkdir(destPath, { recursive: true });
|
|
388
|
-
await copyDir(srcPath, destPath);
|
|
389
|
-
} else {
|
|
390
|
-
await cp(srcPath, destPath);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
362
|
async function updateGitignore() {
|
|
396
363
|
const gitignorePath = resolve(cwd, '.gitignore');
|
|
397
364
|
let content = '';
|
package/index.d.ts
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrinsik/vite-plugin-informer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Vite plugin and publish tool for local Magic Report development",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/entrinsik-org/i5.git",
|
|
8
|
+
"directory": "packages/vite-plugin-informer"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"registry": "https://docker.entrinsik.com/repository/entNPM/"
|
|
12
|
+
},
|
|
13
|
+
"author": "Entrinsik Inc.",
|
|
14
|
+
"license": "UNLICENSED",
|
|
5
15
|
"type": "module",
|
|
6
16
|
"types": "index.d.ts",
|
|
7
17
|
"main": "./src/index.js",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
8
21
|
"exports": {
|
|
9
|
-
".":
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./index.d.ts",
|
|
24
|
+
"default": "./src/index.js"
|
|
25
|
+
},
|
|
10
26
|
"./deploy": "./src/deploy.js"
|
|
11
27
|
},
|
|
12
28
|
"bin": {
|
|
@@ -17,16 +33,12 @@
|
|
|
17
33
|
"files": [
|
|
18
34
|
"src",
|
|
19
35
|
"bin",
|
|
20
|
-
"
|
|
36
|
+
"index.d.ts"
|
|
21
37
|
],
|
|
22
38
|
"peerDependencies": {
|
|
23
39
|
"vite": ">=5.0.0"
|
|
24
40
|
},
|
|
25
41
|
"dependencies": {
|
|
26
|
-
"dotenv": "
|
|
27
|
-
}
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": ">=18.0.0"
|
|
30
|
-
},
|
|
31
|
-
"license": "UNLICENSED"
|
|
42
|
+
"dotenv": "16.6.1"
|
|
43
|
+
}
|
|
32
44
|
}
|
package/src/client.js
CHANGED
|
@@ -60,10 +60,11 @@ export function createClient({ baseUrl, apiKey, user, pass }) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* Upload a file using chunked Flow.js protocol, then assemble into
|
|
64
|
-
* @param {{ reportId
|
|
63
|
+
* Upload a file using chunked Flow.js protocol, then assemble into an entity's library.
|
|
64
|
+
* @param {{ entityPath?: string, reportId?: string, path: string, buffer: Buffer, filename: string }} opts
|
|
65
65
|
*/
|
|
66
|
-
async function uploadChunked({ reportId, path, buffer, filename }) {
|
|
66
|
+
async function uploadChunked({ entityPath, reportId, path, buffer, filename }) {
|
|
67
|
+
const basePath = entityPath || `reports/${reportId}`;
|
|
67
68
|
const totalChunks = Math.ceil(buffer.length / CHUNK_SIZE);
|
|
68
69
|
const uploadId = `publish-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
69
70
|
|
|
@@ -97,8 +98,8 @@ export function createClient({ baseUrl, apiKey, user, pass }) {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
// Assemble chunks into a file in the
|
|
101
|
-
await request('POST',
|
|
101
|
+
// Assemble chunks into a file in the entity's library at the specified path
|
|
102
|
+
await request('POST', `${basePath}/_upload`, {
|
|
102
103
|
uploadId,
|
|
103
104
|
path
|
|
104
105
|
});
|
package/src/deploy.js
CHANGED
|
@@ -14,88 +14,112 @@ const CHUNK_THRESHOLD = 512 * 1024; // 512KB
|
|
|
14
14
|
const ROOT_CONFIG_FILES = ['data-access.yaml'];
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Deploy a built Vite project to Informer as
|
|
17
|
+
* Deploy a built Vite project to Informer as an App (or legacy Magic Report).
|
|
18
|
+
*
|
|
19
|
+
* Tries the new /api/apps endpoint first. If the server doesn't support it,
|
|
20
|
+
* falls back to the legacy /api/reports endpoint transparently.
|
|
18
21
|
*
|
|
19
22
|
* @param {{ baseUrl: string, apiKey?: string, user?: string, pass?: string, distDir: string, name: string, description?: string, icon?: string, id?: string }} opts
|
|
20
|
-
* @returns {Promise<{ id: string, url: string }>} The
|
|
23
|
+
* @returns {Promise<{ id: string, url: string }>} The entity's stable UUID and URL
|
|
21
24
|
*/
|
|
22
25
|
export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, description, icon, id }) {
|
|
23
26
|
const api = createClient({ baseUrl, apiKey, user, pass });
|
|
24
27
|
|
|
25
|
-
let
|
|
28
|
+
let entity = null;
|
|
26
29
|
let naturalId = null;
|
|
30
|
+
let apiPrefix = 'apps';
|
|
27
31
|
|
|
28
|
-
// 1.
|
|
32
|
+
// 1. Try to find existing entity — apps API first, then legacy reports
|
|
29
33
|
if (id) {
|
|
30
|
-
console.log(`Looking up
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
console.log(`Looking up "${id}"...`);
|
|
35
|
+
entity = await api.get(`apps/${id}`);
|
|
36
|
+
if (entity) {
|
|
37
|
+
apiPrefix = 'apps';
|
|
38
|
+
naturalId = entity.naturalId || id;
|
|
39
|
+
console.log(`Found existing app: ${naturalId}`);
|
|
35
40
|
} else {
|
|
36
|
-
|
|
41
|
+
entity = await api.get(`reports/${id}`);
|
|
42
|
+
if (entity) {
|
|
43
|
+
apiPrefix = 'reports';
|
|
44
|
+
naturalId = entity.naturalId || id;
|
|
45
|
+
console.log(`Found existing report: ${naturalId} (legacy API)`);
|
|
46
|
+
} else {
|
|
47
|
+
console.log(`"${id}" not found, will create new...`);
|
|
48
|
+
}
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
// 2. Create if not found
|
|
41
|
-
if (!
|
|
42
|
-
console.log(`Creating new report "${name}"...`);
|
|
52
|
+
// 2. Create if not found — try apps API, fall back to reports
|
|
53
|
+
if (!entity) {
|
|
43
54
|
const payload = {
|
|
44
|
-
type: 'magicReport',
|
|
45
55
|
name: name,
|
|
46
56
|
description: description || ''
|
|
47
57
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
if (id) payload.id = id;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
console.log(`Creating new app "${name}"...`);
|
|
62
|
+
entity = await api.post('apps', { ...payload, type: 'report' });
|
|
63
|
+
apiPrefix = 'apps';
|
|
64
|
+
} catch (e) {
|
|
65
|
+
// If apps endpoint doesn't exist (404), fall back to legacy reports
|
|
66
|
+
if (e.status === 404) {
|
|
67
|
+
console.log('Apps API not available, using legacy reports API...');
|
|
68
|
+
entity = await api.post('reports', { ...payload, type: 'magicReport' });
|
|
69
|
+
apiPrefix = 'reports';
|
|
70
|
+
} else {
|
|
71
|
+
throw e;
|
|
72
|
+
}
|
|
51
73
|
}
|
|
52
|
-
|
|
53
|
-
if (!
|
|
54
|
-
throw new Error(`Failed to create
|
|
74
|
+
|
|
75
|
+
if (!entity || !entity.id) {
|
|
76
|
+
throw new Error(`Failed to create "${name}"`);
|
|
55
77
|
}
|
|
56
|
-
naturalId =
|
|
57
|
-
console.log(`Created
|
|
78
|
+
naturalId = entity.naturalId || `${entity.ownerId}:${entity.slug}`;
|
|
79
|
+
console.log(`Created: ${naturalId}`);
|
|
58
80
|
}
|
|
59
81
|
|
|
60
|
-
|
|
82
|
+
const entityPath = `${apiPrefix}/${naturalId}`;
|
|
83
|
+
|
|
84
|
+
// 3. Update name/description/icon if provided (for existing entities)
|
|
61
85
|
const updates = {};
|
|
62
|
-
if (name &&
|
|
63
|
-
if (description !== undefined &&
|
|
86
|
+
if (name && entity.name !== name) updates.name = name;
|
|
87
|
+
if (description !== undefined && entity.description !== description) updates.description = description;
|
|
64
88
|
if (icon) {
|
|
65
|
-
const currentDefn =
|
|
89
|
+
const currentDefn = entity.defn || {};
|
|
66
90
|
if (currentDefn.icon !== icon) {
|
|
67
91
|
updates.defn = { ...currentDefn, icon };
|
|
68
92
|
}
|
|
69
93
|
}
|
|
70
94
|
if (Object.keys(updates).length > 0) {
|
|
71
|
-
await api.put(
|
|
95
|
+
await api.put(entityPath, updates);
|
|
72
96
|
}
|
|
73
97
|
|
|
74
|
-
//
|
|
98
|
+
// 4. Snapshot
|
|
75
99
|
console.log('Creating snapshot...');
|
|
76
|
-
await api.post(
|
|
100
|
+
await api.post(`${entityPath}/snapshots`, {
|
|
77
101
|
trigger: 'refresh',
|
|
78
102
|
limit: 10
|
|
79
103
|
});
|
|
80
104
|
|
|
81
|
-
//
|
|
105
|
+
// 5. Clear existing files
|
|
82
106
|
console.log('Clearing existing files...');
|
|
83
|
-
const files = await api.get(
|
|
107
|
+
const files = await api.get(`${entityPath}/files?start=0&end=10000`);
|
|
84
108
|
if (files && Array.isArray(files)) {
|
|
85
109
|
// Delete non-directories first
|
|
86
110
|
const nonDirs = files.filter(f => !f.directory);
|
|
87
|
-
await Promise.all(nonDirs.map(f => api.del(
|
|
111
|
+
await Promise.all(nonDirs.map(f => api.del(`${entityPath}/files/${f.id}`)));
|
|
88
112
|
|
|
89
113
|
// Then delete directories in reverse order (deepest first by path length)
|
|
90
114
|
const dirs = files
|
|
91
115
|
.filter(f => f.directory)
|
|
92
116
|
.sort((a, b) => (b.path || '').length - (a.path || '').length);
|
|
93
117
|
for (const d of dirs) {
|
|
94
|
-
await api.del(
|
|
118
|
+
await api.del(`${entityPath}/files/${d.id}`);
|
|
95
119
|
}
|
|
96
120
|
}
|
|
97
121
|
|
|
98
|
-
//
|
|
122
|
+
// 6. Upload dist/ contents
|
|
99
123
|
console.log(`Uploading files from ${distDir}...`);
|
|
100
124
|
const entries = await walkDir(distDir);
|
|
101
125
|
|
|
@@ -106,7 +130,7 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
|
|
|
106
130
|
if (content.length > CHUNK_THRESHOLD) {
|
|
107
131
|
// Large file: chunked upload via Flow.js protocol
|
|
108
132
|
await api.uploadChunked({
|
|
109
|
-
|
|
133
|
+
entityPath,
|
|
110
134
|
path: relPath,
|
|
111
135
|
buffer: content,
|
|
112
136
|
filename: basename(filePath)
|
|
@@ -120,12 +144,12 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
|
|
|
120
144
|
? { content: content.toString('utf8'), encoding: 'utf8' }
|
|
121
145
|
: { content: content.toString('base64'), encoding: 'base64' };
|
|
122
146
|
|
|
123
|
-
await api.put(
|
|
147
|
+
await api.put(`${entityPath}/contents/${relPath}`, payload);
|
|
124
148
|
console.log(` ${relPath}`);
|
|
125
149
|
}
|
|
126
150
|
}
|
|
127
151
|
|
|
128
|
-
//
|
|
152
|
+
// 7. Upload config files from project root (e.g., data-access.yaml)
|
|
129
153
|
const projectRoot = dirname(distDir);
|
|
130
154
|
let configCount = 0;
|
|
131
155
|
for (const configFile of ROOT_CONFIG_FILES) {
|
|
@@ -133,7 +157,7 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
|
|
|
133
157
|
try {
|
|
134
158
|
await access(configPath);
|
|
135
159
|
const content = await readFile(configPath, 'utf8');
|
|
136
|
-
await api.put(
|
|
160
|
+
await api.put(`${entityPath}/contents/${configFile}`, {
|
|
137
161
|
content,
|
|
138
162
|
encoding: 'utf8'
|
|
139
163
|
});
|
|
@@ -144,13 +168,15 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
|
|
|
144
168
|
}
|
|
145
169
|
}
|
|
146
170
|
|
|
147
|
-
//
|
|
171
|
+
// 8. Print URL and return UUID for saving
|
|
148
172
|
const totalFiles = entries.length + configCount;
|
|
149
|
-
const
|
|
150
|
-
|
|
173
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
174
|
+
const entityUrl = apiPrefix === 'apps'
|
|
175
|
+
? `${base}/api/apps/${naturalId}/view`
|
|
176
|
+
: `${base}/reports/r/${naturalId}`;
|
|
177
|
+
console.log(`\nPublished ${totalFiles} files to: ${entityUrl}`);
|
|
151
178
|
|
|
152
|
-
|
|
153
|
-
return { id: report.id, url: reportUrl };
|
|
179
|
+
return { id: entity.id, url: entityUrl };
|
|
154
180
|
}
|
|
155
181
|
|
|
156
182
|
/**
|
package/templates/skill/SKILL.md
DELETED
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: informer
|
|
3
|
-
description: Building Magic Reports with local Vite development. Covers the dev/publish workflow and key Informer APIs for datasets, queries, and integrations.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Magic Report Development
|
|
7
|
-
|
|
8
|
-
## What is a Magic Report?
|
|
9
|
-
|
|
10
|
-
A Magic Report is a custom HTML/JS/CSS application that runs inside Informer. It can:
|
|
11
|
-
- Query Informer datasets (Elasticsearch-indexed data)
|
|
12
|
-
- Execute saved queries
|
|
13
|
-
- Make authenticated requests to external APIs via integrations (Salesforce, etc.)
|
|
14
|
-
- Render charts, tables, and interactive visualizations
|
|
15
|
-
|
|
16
|
-
Reports are stored in Informer libraries and served through the Informer UI.
|
|
17
|
-
|
|
18
|
-
## Local Development Workflow
|
|
19
|
-
|
|
20
|
-
### Development Mode (`npm run dev`)
|
|
21
|
-
|
|
22
|
-
The Vite plugin proxies `/api/*` requests to your Informer server with Basic auth. This means:
|
|
23
|
-
- Your code makes fetch calls to `/api/...` (no host needed)
|
|
24
|
-
- The plugin adds authentication headers automatically
|
|
25
|
-
- You get hot reload while working against real Informer data
|
|
26
|
-
|
|
27
|
-
Configuration is in `.env`:
|
|
28
|
-
```
|
|
29
|
-
INFORMER_URL=http://localhost:3000
|
|
30
|
-
INFORMER_API_KEY=your-api-key
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Or use basic auth:
|
|
34
|
-
```
|
|
35
|
-
INFORMER_URL=http://localhost:3000
|
|
36
|
-
INFORMER_USER=admin
|
|
37
|
-
INFORMER_PASS=yourpassword
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Deploying (`npm run deploy`)
|
|
41
|
-
|
|
42
|
-
Builds your project and uploads to an Informer library:
|
|
43
|
-
1. Creates/finds a Magic Report by slug (from package.json `name`)
|
|
44
|
-
2. Snapshots the library for rollback
|
|
45
|
-
3. Clears existing files
|
|
46
|
-
4. Uploads all built assets from `dist/`
|
|
47
|
-
5. Uploads `data-access.yaml` from project root (if it exists)
|
|
48
|
-
6. Report is viewable at `/reports/r/{owner}:{slug}`
|
|
49
|
-
|
|
50
|
-
### Package.json Configuration
|
|
51
|
-
|
|
52
|
-
The `informer` section in `package.json` controls deploy metadata:
|
|
53
|
-
|
|
54
|
-
```json
|
|
55
|
-
{
|
|
56
|
-
"informer": {
|
|
57
|
-
"name": "Sales Dashboard",
|
|
58
|
-
"description": "Regional sales performance overview",
|
|
59
|
-
"icon": "BarChart",
|
|
60
|
-
"id": "a1b2c3d4-..."
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
| Field | Description |
|
|
66
|
-
|-------|-------------|
|
|
67
|
-
| `name` | Display name in Informer (falls back to package `name`) |
|
|
68
|
-
| `description` | Report description |
|
|
69
|
-
| `icon` | Material Icons name in PascalCase (e.g. `"TrendingUp"`, `"PieChart"`, `"Assessment"`) |
|
|
70
|
-
| `id` | Report UUID (auto-saved after first deploy) |
|
|
71
|
-
|
|
72
|
-
## Discovering Resources
|
|
73
|
-
|
|
74
|
-
Once `.env` is configured, Claude can query the Informer API directly to help you find available resources. Ask Claude to look up:
|
|
75
|
-
|
|
76
|
-
- **Integrations**: `curl -u $USER:$PASS "$INFORMER_URL/api/integrations"` - Find integration slugs for QuickBooks, Salesforce, etc.
|
|
77
|
-
- **Datasets**: `curl -u $USER:$PASS "$INFORMER_URL/api/datasets-list"` - Find dataset IDs and field names
|
|
78
|
-
- **Queries**: `curl -u $USER:$PASS "$INFORMER_URL/api/queries-list"` - Find saved query IDs
|
|
79
|
-
- **Datasources**: `curl -u $USER:$PASS "$INFORMER_URL/api/datasources"` - Find SQL datasource IDs
|
|
80
|
-
|
|
81
|
-
This helps you find the correct IDs/slugs to use in your code and `data-access.yaml`. Just ask Claude to "show me available integrations" or "find the QuickBooks integration slug".
|
|
82
|
-
|
|
83
|
-
## Key APIs
|
|
84
|
-
|
|
85
|
-
All endpoints are relative to `/api`. In dev mode, the Vite proxy handles auth.
|
|
86
|
-
|
|
87
|
-
### List Datasets
|
|
88
|
-
|
|
89
|
-
```javascript
|
|
90
|
-
const response = await fetch('/api/datasets-list');
|
|
91
|
-
const datasets = await response.json();
|
|
92
|
-
// Returns: [{ id, name, description, records, size, ... }, ...]
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Use this to discover available datasets. Each dataset has:
|
|
96
|
-
- `id` - UUID or natural ID like `admin:sales-data`
|
|
97
|
-
- `name` - Display name
|
|
98
|
-
- `records` - Approximate record count
|
|
99
|
-
|
|
100
|
-
### Search Dataset (Elasticsearch)
|
|
101
|
-
|
|
102
|
-
```javascript
|
|
103
|
-
const response = await fetch(`/api/datasets/${datasetId}/_search`, {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
headers: { 'Content-Type': 'application/json' },
|
|
106
|
-
body: JSON.stringify({
|
|
107
|
-
query: { match_all: {} },
|
|
108
|
-
size: 100,
|
|
109
|
-
from: 0,
|
|
110
|
-
_source: ['field1', 'field2'], // Optional: limit fields returned
|
|
111
|
-
sort: [{ field1: 'desc' }], // Optional: sort order
|
|
112
|
-
aggs: { // Optional: aggregations
|
|
113
|
-
total: { sum: { field: 'amount' } }
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
});
|
|
117
|
-
const result = await response.json();
|
|
118
|
-
|
|
119
|
-
// Response structure:
|
|
120
|
-
// result.hits.total - total matching records
|
|
121
|
-
// result.hits.hits - array of { _source: { field1, field2, ... } }
|
|
122
|
-
// result.aggregations - aggregation results (if requested)
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
**Common query patterns:**
|
|
126
|
-
|
|
127
|
-
```javascript
|
|
128
|
-
// Filter by exact value
|
|
129
|
-
{ query: { bool: { filter: [{ term: { status: 'active' } }] } } }
|
|
130
|
-
|
|
131
|
-
// Filter by range
|
|
132
|
-
{ query: { bool: { filter: [{ range: { amount: { gte: 1000 } } }] } } }
|
|
133
|
-
|
|
134
|
-
// Date range
|
|
135
|
-
{ query: { bool: { filter: [{ range: { date: { gte: '2024-01-01', lte: '2024-12-31' } } }] } } }
|
|
136
|
-
|
|
137
|
-
// Multiple filters (AND)
|
|
138
|
-
{ query: { bool: { filter: [
|
|
139
|
-
{ term: { region: 'North' } },
|
|
140
|
-
{ range: { amount: { gte: 1000 } } }
|
|
141
|
-
] } } }
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
**Common aggregations:**
|
|
145
|
-
|
|
146
|
-
```javascript
|
|
147
|
-
// Sum, avg, min, max
|
|
148
|
-
{ aggs: { total: { sum: { field: 'amount' } } } }
|
|
149
|
-
|
|
150
|
-
// Group by field
|
|
151
|
-
{ aggs: { by_region: { terms: { field: 'region', size: 50 } } } }
|
|
152
|
-
|
|
153
|
-
// Group with nested metric
|
|
154
|
-
{ aggs: {
|
|
155
|
-
by_region: {
|
|
156
|
-
terms: { field: 'region', size: 50 },
|
|
157
|
-
aggs: { total: { sum: { field: 'amount' } } }
|
|
158
|
-
}
|
|
159
|
-
} }
|
|
160
|
-
|
|
161
|
-
// Date histogram
|
|
162
|
-
{ aggs: {
|
|
163
|
-
by_month: {
|
|
164
|
-
date_histogram: { field: 'date', calendar_interval: 'month' },
|
|
165
|
-
aggs: { total: { sum: { field: 'amount' } } }
|
|
166
|
-
}
|
|
167
|
-
} }
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### List Queries
|
|
171
|
-
|
|
172
|
-
```javascript
|
|
173
|
-
const response = await fetch('/api/queries-list');
|
|
174
|
-
const queries = await response.json();
|
|
175
|
-
// Returns: [{ id, name, description, ... }, ...]
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Execute Query
|
|
179
|
-
|
|
180
|
-
```javascript
|
|
181
|
-
const response = await fetch(`/api/queries/${queryId}/_execute`, {
|
|
182
|
-
method: 'POST',
|
|
183
|
-
headers: { 'Content-Type': 'application/json' },
|
|
184
|
-
body: JSON.stringify({
|
|
185
|
-
parameters: { param1: 'value1' } // Optional query parameters
|
|
186
|
-
})
|
|
187
|
-
});
|
|
188
|
-
const result = await response.json();
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### List Integrations
|
|
192
|
-
|
|
193
|
-
```javascript
|
|
194
|
-
const response = await fetch('/api/integrations');
|
|
195
|
-
const result = await response.json();
|
|
196
|
-
// result.items = [{ id, name, slug, type, ... }, ...]
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
Integrations are authenticated connections to external APIs (Salesforce, REST APIs, etc.).
|
|
200
|
-
|
|
201
|
-
### Make Integration Request
|
|
202
|
-
|
|
203
|
-
```javascript
|
|
204
|
-
const response = await fetch(`/api/integrations/${slugOrId}/request`, {
|
|
205
|
-
method: 'POST',
|
|
206
|
-
headers: { 'Content-Type': 'application/json' },
|
|
207
|
-
body: JSON.stringify({
|
|
208
|
-
url: '/data/v59.0/query', // Path relative to integration's base URL
|
|
209
|
-
method: 'GET', // HTTP method
|
|
210
|
-
params: { q: 'SELECT Id FROM Account' }, // Query params
|
|
211
|
-
data: { /* body for POST/PUT */ }, // Request body
|
|
212
|
-
headers: { /* extra headers */ } // Additional headers
|
|
213
|
-
})
|
|
214
|
-
});
|
|
215
|
-
const result = await response.json();
|
|
216
|
-
|
|
217
|
-
// Response structure:
|
|
218
|
-
// result.status - HTTP status code
|
|
219
|
-
// result.data - response body from the external API
|
|
220
|
-
// result.error - true if upstream returned an error status
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
**Salesforce example:**
|
|
224
|
-
```javascript
|
|
225
|
-
const response = await fetch('/api/integrations/salesforce/request', {
|
|
226
|
-
method: 'POST',
|
|
227
|
-
headers: { 'Content-Type': 'application/json' },
|
|
228
|
-
body: JSON.stringify({
|
|
229
|
-
url: '/data/v59.0/query',
|
|
230
|
-
method: 'GET',
|
|
231
|
-
params: {
|
|
232
|
-
q: "SELECT Id, Name, Amount FROM Opportunity WHERE StageName = 'Closed Won'"
|
|
233
|
-
}
|
|
234
|
-
})
|
|
235
|
-
});
|
|
236
|
-
const result = await response.json();
|
|
237
|
-
const records = result.data.records;
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
## Data Access Configuration
|
|
241
|
-
|
|
242
|
-
When your report is published and shared, you must declare which APIs it needs access to. Create a `data-access.yaml` file in your project root (it will be published with your report).
|
|
243
|
-
|
|
244
|
-
**Important:** Without this file, all API access is blocked when the report runs in Informer.
|
|
245
|
-
|
|
246
|
-
### Basic Example
|
|
247
|
-
|
|
248
|
-
```yaml
|
|
249
|
-
# data-access.yaml
|
|
250
|
-
|
|
251
|
-
datasets:
|
|
252
|
-
- admin:sales-data
|
|
253
|
-
- admin:customers
|
|
254
|
-
|
|
255
|
-
queries:
|
|
256
|
-
- admin:monthly-summary
|
|
257
|
-
|
|
258
|
-
integrations:
|
|
259
|
-
- salesforce
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### With Row-Level Security
|
|
263
|
-
|
|
264
|
-
Restrict data based on the viewing user's profile:
|
|
265
|
-
|
|
266
|
-
```yaml
|
|
267
|
-
datasets:
|
|
268
|
-
# Users only see their region's data
|
|
269
|
-
- id: admin:orders
|
|
270
|
-
filter:
|
|
271
|
-
region: $user.custom.region
|
|
272
|
-
|
|
273
|
-
# Users only see their own records
|
|
274
|
-
- id: admin:sales
|
|
275
|
-
filter:
|
|
276
|
-
sales_rep: $user.username
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### Integration with Credentials
|
|
280
|
-
|
|
281
|
-
Pass user-specific credentials to external APIs:
|
|
282
|
-
|
|
283
|
-
```yaml
|
|
284
|
-
integrations:
|
|
285
|
-
- id: partner-api
|
|
286
|
-
headers:
|
|
287
|
-
Authorization: Bearer $user.custom.partnerToken
|
|
288
|
-
params:
|
|
289
|
-
client_id: $tenant.id
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Available Variables
|
|
293
|
-
|
|
294
|
-
| Variable | Description |
|
|
295
|
-
|----------|-------------|
|
|
296
|
-
| `$user.username` | Login name |
|
|
297
|
-
| `$user.email` | Email address |
|
|
298
|
-
| `$user.displayName` | Full name |
|
|
299
|
-
| `$user.custom.xxx` | Custom user field |
|
|
300
|
-
| `$tenant.id` | Tenant ID |
|
|
301
|
-
| `$report.id` | Report UUID |
|
|
302
|
-
|
|
303
|
-
### Resource Types
|
|
304
|
-
|
|
305
|
-
| Type | API Access Granted |
|
|
306
|
-
|------|-------------------|
|
|
307
|
-
| `datasets` | `_search`, `fields` |
|
|
308
|
-
| `queries` | `_execute` |
|
|
309
|
-
| `datasources` | `_query` |
|
|
310
|
-
| `integrations` | `request` |
|
|
311
|
-
| `libraries` | `contents/*` |
|
|
312
|
-
|
|
313
|
-
For edge cases, you can also whitelist raw API paths:
|
|
314
|
-
|
|
315
|
-
```yaml
|
|
316
|
-
apis:
|
|
317
|
-
- POST /api/custom/endpoint
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
## Report Context
|
|
321
|
-
|
|
322
|
-
When running inside Informer (not dev mode), the report receives context:
|
|
323
|
-
|
|
324
|
-
```javascript
|
|
325
|
-
const reportId = window.__INFORMER__?.report?.id;
|
|
326
|
-
const reportName = window.__INFORMER__?.report?.name;
|
|
327
|
-
const theme = window.__INFORMER__?.theme; // 'light' or 'dark'
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
In dev mode, the Vite plugin mocks this with placeholder values (theme defaults to `'light'`).
|
|
331
|
-
|
|
332
|
-
### Responding to Theme
|
|
333
|
-
|
|
334
|
-
Use the theme value to adapt your report's appearance:
|
|
335
|
-
|
|
336
|
-
```javascript
|
|
337
|
-
const theme = window.__INFORMER__?.theme || 'light';
|
|
338
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
```css
|
|
342
|
-
:root { --bg: #ffffff; --text: #1a1a1a; }
|
|
343
|
-
[data-theme="dark"] { --bg: #1e1e1e; --text: #e0e0e0; }
|
|
344
|
-
body { background: var(--bg); color: var(--text); }
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
To override the theme in dev mode, pass `mock.theme` in `vite.config.js`:
|
|
348
|
-
|
|
349
|
-
```javascript
|
|
350
|
-
import informer from 'vite-plugin-informer';
|
|
351
|
-
|
|
352
|
-
export default {
|
|
353
|
-
plugins: [informer({ mock: { theme: 'dark' } })]
|
|
354
|
-
};
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
## PDF Export
|
|
358
|
-
|
|
359
|
-
Reports can be exported to PDF via `POST /api/reports/{id}/_print`.
|
|
360
|
-
|
|
361
|
-
### How it works
|
|
362
|
-
|
|
363
|
-
1. Informer opens your report in a headless browser (Puppeteer)
|
|
364
|
-
2. Waits for network requests to complete
|
|
365
|
-
3. Waits for `window.informerReady` to become `true`
|
|
366
|
-
4. Adds `.print` class to `<html>`
|
|
367
|
-
5. Captures the page as PDF using print media
|
|
368
|
-
|
|
369
|
-
### Signal when ready
|
|
370
|
-
|
|
371
|
-
Set `window.informerReady` to signal when your report is fully rendered:
|
|
372
|
-
|
|
373
|
-
```javascript
|
|
374
|
-
// Start of app
|
|
375
|
-
window.informerReady = false;
|
|
376
|
-
|
|
377
|
-
// After all charts/content rendered
|
|
378
|
-
window.informerReady = true;
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
### Rendering details
|
|
382
|
-
|
|
383
|
-
- **Print media is used** - Standard `@media print` CSS rules apply
|
|
384
|
-
- **`.print` class added** - Informer adds a `.print` class to `<html>` for additional targeting
|
|
385
|
-
- **Viewport is 1200px** by default (configurable via `viewportWidth` option)
|
|
386
|
-
- **Box shadows are removed** - They render as grey boxes in PDFs
|
|
387
|
-
- **Colors are preserved** - `print-color-adjust: exact` is applied automatically
|
|
388
|
-
|
|
389
|
-
### Print CSS
|
|
390
|
-
|
|
391
|
-
Use standard `@media print` rules or the `.print` class:
|
|
392
|
-
|
|
393
|
-
```css
|
|
394
|
-
/* Standard print media query */
|
|
395
|
-
@media print {
|
|
396
|
-
body {
|
|
397
|
-
background: white;
|
|
398
|
-
color: black;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.no-print {
|
|
402
|
-
display: none;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/* Or use the .print class (added by Informer) */
|
|
407
|
-
.print .no-print {
|
|
408
|
-
display: none;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/* Avoid page breaks inside elements */
|
|
412
|
-
.chart-container {
|
|
413
|
-
break-inside: avoid;
|
|
414
|
-
}
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
### Print API options
|
|
418
|
-
|
|
419
|
-
```javascript
|
|
420
|
-
await fetch(`/api/reports/${reportId}/_print`, {
|
|
421
|
-
method: 'POST',
|
|
422
|
-
headers: { 'Content-Type': 'application/json' },
|
|
423
|
-
body: JSON.stringify({
|
|
424
|
-
format: 'Letter', // Letter, Legal, Tabloid, A3, A4, A5
|
|
425
|
-
landscape: false,
|
|
426
|
-
viewportWidth: 1200, // 400-2400, affects responsive layouts
|
|
427
|
-
waitForReady: true, // Wait for window.informerReady
|
|
428
|
-
save: false // true = save to downloads, false = return PDF
|
|
429
|
-
})
|
|
430
|
-
});
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
## Reference Files
|
|
434
|
-
|
|
435
|
-
- `references/api-reference.md` - Detailed API documentation
|
|
436
|
-
- `references/report-templates.md` - HTML/CSS/JS starter templates
|
|
@@ -1,242 +0,0 @@
|
|
|
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)
|
|
@@ -1,299 +0,0 @@
|
|
|
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
|
-
```
|