@bonnard/cli 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/bin/bon.mjs +173 -2
- package/dist/docs/topics/sdk.apexcharts.md +281 -0
- package/dist/docs/topics/sdk.authentication.md +130 -0
- package/dist/docs/topics/sdk.browser.md +181 -0
- package/dist/docs/topics/sdk.chartjs.md +327 -0
- package/dist/docs/topics/sdk.echarts.md +297 -0
- package/dist/docs/topics/sdk.md +95 -0
- package/dist/docs/topics/sdk.query-reference.md +307 -0
- package/dist/templates/claude/skills/bonnard-build-dashboard/SKILL.md +107 -0
- package/dist/templates/claude/skills/bonnard-get-started/SKILL.md +1 -1
- package/dist/templates/claude/skills/bonnard-metabase-migrate/SKILL.md +1 -1
- package/dist/templates/cursor/rules/bonnard-build-dashboard.mdc +106 -0
- package/dist/templates/cursor/rules/bonnard-get-started.mdc +1 -1
- package/dist/templates/cursor/rules/bonnard-metabase-migrate.mdc +1 -1
- package/dist/templates/shared/bonnard.md +6 -1
- package/package.json +2 -2
- package/dist/docs/topics/dashboards.components.md +0 -227
- package/dist/docs/topics/dashboards.examples.md +0 -270
- package/dist/docs/topics/dashboards.inputs.md +0 -173
- package/dist/docs/topics/dashboards.md +0 -115
- package/dist/docs/topics/dashboards.queries.md +0 -112
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ Agent support:
|
|
|
98
98
|
Set up your MCP server so agents can query your semantic layer directly:
|
|
99
99
|
|
|
100
100
|
```bash
|
|
101
|
-
bon mcp
|
|
101
|
+
bon mcp # Show MCP server setup instructions
|
|
102
102
|
bon mcp test # Verify the connection
|
|
103
103
|
```
|
|
104
104
|
|
|
@@ -172,7 +172,7 @@ Handles automatic datasource synchronisation and skips interactive prompts. Fits
|
|
|
172
172
|
| `bon diff` | Preview changes before deploying |
|
|
173
173
|
| `bon annotate` | Add metadata and descriptions to models |
|
|
174
174
|
| `bon query` | Run queries from the terminal (JSON or SQL) |
|
|
175
|
-
| `bon mcp
|
|
175
|
+
| `bon mcp` | Show MCP server setup instructions |
|
|
176
176
|
| `bon mcp test` | Test MCP connection |
|
|
177
177
|
| `bon docs` | Browse or search documentation from the CLI |
|
|
178
178
|
| `bon login` / `bon logout` | Manage authentication |
|
package/dist/bin/bon.mjs
CHANGED
|
@@ -13,6 +13,8 @@ import YAML from "yaml";
|
|
|
13
13
|
import http from "node:http";
|
|
14
14
|
import crypto from "node:crypto";
|
|
15
15
|
import { encode } from "@toon-format/toon";
|
|
16
|
+
import { createInterface } from "node:readline";
|
|
17
|
+
import open from "open";
|
|
16
18
|
|
|
17
19
|
//#region \0rolldown/runtime.js
|
|
18
20
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
@@ -611,10 +613,12 @@ function createAgentTemplates(cwd, env) {
|
|
|
611
613
|
fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-get-started"), { recursive: true });
|
|
612
614
|
fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-metabase-migrate"), { recursive: true });
|
|
613
615
|
fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-design-guide"), { recursive: true });
|
|
616
|
+
fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-build-dashboard"), { recursive: true });
|
|
614
617
|
writeTemplateFile(sharedBonnard, path.join(claudeRulesDir, "bonnard.md"), createdFiles);
|
|
615
618
|
writeTemplateFile(loadTemplate("claude/skills/bonnard-get-started/SKILL.md"), path.join(claudeSkillsDir, "bonnard-get-started", "SKILL.md"), createdFiles);
|
|
616
619
|
writeTemplateFile(loadTemplate("claude/skills/bonnard-metabase-migrate/SKILL.md"), path.join(claudeSkillsDir, "bonnard-metabase-migrate", "SKILL.md"), createdFiles);
|
|
617
620
|
writeTemplateFile(loadTemplate("claude/skills/bonnard-design-guide/SKILL.md"), path.join(claudeSkillsDir, "bonnard-design-guide", "SKILL.md"), createdFiles);
|
|
621
|
+
writeTemplateFile(loadTemplate("claude/skills/bonnard-build-dashboard/SKILL.md"), path.join(claudeSkillsDir, "bonnard-build-dashboard", "SKILL.md"), createdFiles);
|
|
618
622
|
mergeSettingsJson(loadJsonTemplate("claude/settings.json"), path.join(cwd, ".claude", "settings.json"), createdFiles);
|
|
619
623
|
const cursorRulesDir = path.join(cwd, ".cursor", "rules");
|
|
620
624
|
fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
@@ -622,14 +626,17 @@ function createAgentTemplates(cwd, env) {
|
|
|
622
626
|
writeTemplateFile(loadTemplate("cursor/rules/bonnard-get-started.mdc"), path.join(cursorRulesDir, "bonnard-get-started.mdc"), createdFiles);
|
|
623
627
|
writeTemplateFile(loadTemplate("cursor/rules/bonnard-metabase-migrate.mdc"), path.join(cursorRulesDir, "bonnard-metabase-migrate.mdc"), createdFiles);
|
|
624
628
|
writeTemplateFile(loadTemplate("cursor/rules/bonnard-design-guide.mdc"), path.join(cursorRulesDir, "bonnard-design-guide.mdc"), createdFiles);
|
|
629
|
+
writeTemplateFile(loadTemplate("cursor/rules/bonnard-build-dashboard.mdc"), path.join(cursorRulesDir, "bonnard-build-dashboard.mdc"), createdFiles);
|
|
625
630
|
const codexSkillsDir = path.join(cwd, ".agents", "skills");
|
|
626
631
|
fs.mkdirSync(path.join(codexSkillsDir, "bonnard-get-started"), { recursive: true });
|
|
627
632
|
fs.mkdirSync(path.join(codexSkillsDir, "bonnard-metabase-migrate"), { recursive: true });
|
|
628
633
|
fs.mkdirSync(path.join(codexSkillsDir, "bonnard-design-guide"), { recursive: true });
|
|
634
|
+
fs.mkdirSync(path.join(codexSkillsDir, "bonnard-build-dashboard"), { recursive: true });
|
|
629
635
|
writeTemplateFile(sharedBonnard, path.join(cwd, "AGENTS.md"), createdFiles);
|
|
630
636
|
writeTemplateFile(loadTemplate("claude/skills/bonnard-get-started/SKILL.md"), path.join(codexSkillsDir, "bonnard-get-started", "SKILL.md"), createdFiles);
|
|
631
637
|
writeTemplateFile(loadTemplate("claude/skills/bonnard-metabase-migrate/SKILL.md"), path.join(codexSkillsDir, "bonnard-metabase-migrate", "SKILL.md"), createdFiles);
|
|
632
638
|
writeTemplateFile(loadTemplate("claude/skills/bonnard-design-guide/SKILL.md"), path.join(codexSkillsDir, "bonnard-design-guide", "SKILL.md"), createdFiles);
|
|
639
|
+
writeTemplateFile(loadTemplate("claude/skills/bonnard-build-dashboard/SKILL.md"), path.join(codexSkillsDir, "bonnard-build-dashboard", "SKILL.md"), createdFiles);
|
|
633
640
|
return createdFiles;
|
|
634
641
|
}
|
|
635
642
|
async function initCommand() {
|
|
@@ -670,12 +677,12 @@ async function initCommand() {
|
|
|
670
677
|
|
|
671
678
|
//#endregion
|
|
672
679
|
//#region src/commands/login.ts
|
|
673
|
-
const APP_URL = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
680
|
+
const APP_URL$3 = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
674
681
|
const TIMEOUT_MS = 120 * 1e3;
|
|
675
682
|
async function loginCommand() {
|
|
676
683
|
const state = crypto.randomUUID();
|
|
677
684
|
const { port, close } = await startCallbackServer(state);
|
|
678
|
-
const url = `${APP_URL}/auth/device?state=${state}&port=${port}`;
|
|
685
|
+
const url = `${APP_URL$3}/auth/device?state=${state}&port=${port}`;
|
|
679
686
|
console.log(pc.dim(`Opening browser to ${url}`));
|
|
680
687
|
const open = (await import("open")).default;
|
|
681
688
|
await open(url);
|
|
@@ -1908,6 +1915,30 @@ async function annotateCommand(id, options = {}) {
|
|
|
1908
1915
|
}
|
|
1909
1916
|
}
|
|
1910
1917
|
|
|
1918
|
+
//#endregion
|
|
1919
|
+
//#region src/commands/pull.ts
|
|
1920
|
+
async function pullCommand() {
|
|
1921
|
+
const paths = getProjectPaths(process.cwd());
|
|
1922
|
+
console.log(pc.dim("Downloading deployed models..."));
|
|
1923
|
+
try {
|
|
1924
|
+
const files = (await get("/api/deploy/files")).files;
|
|
1925
|
+
const fileKeys = Object.keys(files);
|
|
1926
|
+
if (fileKeys.length === 0) {
|
|
1927
|
+
console.log(pc.yellow("No deployed files found. Have you run `bon deploy` yet?"));
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
1931
|
+
const fullPath = path.join(paths.root, relativePath);
|
|
1932
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
1933
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
1934
|
+
}
|
|
1935
|
+
console.log(pc.green(`Pulled ${fileKeys.length} file(s) to bonnard/`));
|
|
1936
|
+
} catch (err) {
|
|
1937
|
+
console.log(pc.red(`Pull failed: ${err instanceof Error ? err.message : err}`));
|
|
1938
|
+
process.exit(1);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1911
1942
|
//#endregion
|
|
1912
1943
|
//#region src/commands/diff.ts
|
|
1913
1944
|
async function diffCommand(id, options = {}) {
|
|
@@ -3846,6 +3877,140 @@ async function keysRevokeCommand(nameOrPrefix) {
|
|
|
3846
3877
|
}
|
|
3847
3878
|
}
|
|
3848
3879
|
|
|
3880
|
+
//#endregion
|
|
3881
|
+
//#region src/commands/dashboard/deploy.ts
|
|
3882
|
+
const APP_URL$2 = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
3883
|
+
/**
|
|
3884
|
+
* Extract <title> content from HTML string
|
|
3885
|
+
*/
|
|
3886
|
+
function extractTitle(html) {
|
|
3887
|
+
const match = html.match(/<title[^>]*>(.*?)<\/title>/is);
|
|
3888
|
+
return match ? match[1].trim() : null;
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Derive slug from filename: strip extension, lowercase, replace non-alphanumeric with hyphens
|
|
3892
|
+
*/
|
|
3893
|
+
function slugFromFilename(filename) {
|
|
3894
|
+
return path.basename(filename, path.extname(filename)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
3895
|
+
}
|
|
3896
|
+
async function dashboardDeployCommand(file, options) {
|
|
3897
|
+
const filePath = path.resolve(file);
|
|
3898
|
+
if (!fs.existsSync(filePath)) {
|
|
3899
|
+
console.error(pc.red(`File not found: ${file}`));
|
|
3900
|
+
process.exit(1);
|
|
3901
|
+
}
|
|
3902
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
3903
|
+
if (content.length > 2 * 1024 * 1024) {
|
|
3904
|
+
console.error(pc.red("File exceeds 2MB limit."));
|
|
3905
|
+
process.exit(1);
|
|
3906
|
+
}
|
|
3907
|
+
const slug = options.slug || slugFromFilename(file);
|
|
3908
|
+
const title = options.title || extractTitle(content) || slug;
|
|
3909
|
+
console.log(pc.dim(`Deploying dashboard "${slug}"...`));
|
|
3910
|
+
try {
|
|
3911
|
+
const dashboard = (await post("/api/dashboards", {
|
|
3912
|
+
slug,
|
|
3913
|
+
title,
|
|
3914
|
+
content
|
|
3915
|
+
})).dashboard;
|
|
3916
|
+
const url = `${APP_URL$2}/d/${dashboard.org_slug}/${dashboard.slug}`;
|
|
3917
|
+
console.log(pc.green(`Dashboard deployed successfully`));
|
|
3918
|
+
console.log();
|
|
3919
|
+
console.log(` ${pc.bold("Slug")} ${dashboard.slug}`);
|
|
3920
|
+
console.log(` ${pc.bold("Title")} ${dashboard.title}`);
|
|
3921
|
+
console.log(` ${pc.bold("Version")} ${dashboard.version}`);
|
|
3922
|
+
console.log(` ${pc.bold("URL")} ${url}`);
|
|
3923
|
+
} catch (err) {
|
|
3924
|
+
console.error(pc.red(`Deploy failed: ${err instanceof Error ? err.message : err}`));
|
|
3925
|
+
process.exit(1);
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
//#endregion
|
|
3930
|
+
//#region src/commands/dashboard/list.ts
|
|
3931
|
+
const APP_URL$1 = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
3932
|
+
async function dashboardListCommand() {
|
|
3933
|
+
try {
|
|
3934
|
+
const dashboards = (await get("/api/dashboards")).dashboards;
|
|
3935
|
+
if (dashboards.length === 0) {
|
|
3936
|
+
console.log(pc.dim("No dashboards deployed yet. Run `bon dashboard deploy <file>` to publish."));
|
|
3937
|
+
return;
|
|
3938
|
+
}
|
|
3939
|
+
const slugWidth = Math.max(4, ...dashboards.map((d) => d.slug.length));
|
|
3940
|
+
const titleWidth = Math.max(5, ...dashboards.map((d) => d.title.length));
|
|
3941
|
+
const versionWidth = 7;
|
|
3942
|
+
const dateWidth = 10;
|
|
3943
|
+
const header = [
|
|
3944
|
+
"SLUG".padEnd(slugWidth),
|
|
3945
|
+
"TITLE".padEnd(titleWidth),
|
|
3946
|
+
"VERSION".padEnd(versionWidth),
|
|
3947
|
+
"UPDATED".padEnd(dateWidth),
|
|
3948
|
+
"URL"
|
|
3949
|
+
].join(" ");
|
|
3950
|
+
console.log(pc.dim(header));
|
|
3951
|
+
for (const d of dashboards) {
|
|
3952
|
+
const url = `${APP_URL$1}/d/${d.org_slug}/${d.slug}`;
|
|
3953
|
+
const date = new Date(d.updated_at).toLocaleDateString();
|
|
3954
|
+
const row = [
|
|
3955
|
+
d.slug.padEnd(slugWidth),
|
|
3956
|
+
d.title.padEnd(titleWidth),
|
|
3957
|
+
`v${d.version}`.padEnd(versionWidth),
|
|
3958
|
+
date.padEnd(dateWidth),
|
|
3959
|
+
url
|
|
3960
|
+
].join(" ");
|
|
3961
|
+
console.log(row);
|
|
3962
|
+
}
|
|
3963
|
+
} catch (err) {
|
|
3964
|
+
console.error(pc.red(`Failed to list dashboards: ${err instanceof Error ? err.message : err}`));
|
|
3965
|
+
process.exit(1);
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
//#endregion
|
|
3970
|
+
//#region src/commands/dashboard/remove.ts
|
|
3971
|
+
async function confirm(message) {
|
|
3972
|
+
const rl = createInterface({
|
|
3973
|
+
input: process.stdin,
|
|
3974
|
+
output: process.stdout
|
|
3975
|
+
});
|
|
3976
|
+
return new Promise((resolve) => {
|
|
3977
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
3978
|
+
rl.close();
|
|
3979
|
+
resolve(answer.toLowerCase() === "y");
|
|
3980
|
+
});
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
async function dashboardRemoveCommand(slug, options) {
|
|
3984
|
+
if (!options.force) {
|
|
3985
|
+
if (!await confirm(`Remove dashboard "${slug}"? This cannot be undone.`)) {
|
|
3986
|
+
console.log(pc.dim("Cancelled."));
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
try {
|
|
3991
|
+
await del(`/api/dashboards/${encodeURIComponent(slug)}`);
|
|
3992
|
+
console.log(pc.green(`Dashboard "${slug}" removed.`));
|
|
3993
|
+
} catch (err) {
|
|
3994
|
+
console.error(pc.red(`Failed to remove dashboard: ${err instanceof Error ? err.message : err}`));
|
|
3995
|
+
process.exit(1);
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
//#endregion
|
|
4000
|
+
//#region src/commands/dashboard/open.ts
|
|
4001
|
+
const APP_URL = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
4002
|
+
async function dashboardOpenCommand(slug) {
|
|
4003
|
+
try {
|
|
4004
|
+
const result = await get(`/api/dashboards/${encodeURIComponent(slug)}`);
|
|
4005
|
+
const url = `${APP_URL}/d/${result.dashboard.org_slug}/${result.dashboard.slug}`;
|
|
4006
|
+
console.log(pc.dim(`Opening ${url}`));
|
|
4007
|
+
await open(url);
|
|
4008
|
+
} catch (err) {
|
|
4009
|
+
console.error(pc.red(`Failed to open dashboard: ${err instanceof Error ? err.message : err}`));
|
|
4010
|
+
process.exit(1);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
|
|
3849
4014
|
//#endregion
|
|
3850
4015
|
//#region src/bin/bon.ts
|
|
3851
4016
|
const { version } = createRequire(import.meta.url)("../../package.json");
|
|
@@ -3860,6 +4025,7 @@ datasource.command("list").description("List data sources (shows both local and
|
|
|
3860
4025
|
datasource.command("remove").description("Remove a data source from .bon/datasources.yaml (local by default)").argument("<name>", "Data source name").option("--remote", "Remove from Bonnard server instead of local (requires login)").action(datasourceRemoveCommand);
|
|
3861
4026
|
program.command("validate").description("Validate YAML syntax in bonnard/cubes/ and bonnard/views/").action(validateCommand);
|
|
3862
4027
|
program.command("deploy").description("Deploy cubes and views to Bonnard. Requires login, validates, syncs datasources").option("--ci", "Non-interactive mode").requiredOption("-m, --message <text>", "Deploy message describing your changes").action(deployCommand);
|
|
4028
|
+
program.command("pull").description("Download deployed cubes and views from Bonnard").action(pullCommand);
|
|
3863
4029
|
program.command("deployments").description("List deployment history").option("--all", "Show all deployments (default: last 10)").option("--format <format>", "Output format: table or json", "table").action(deploymentsCommand);
|
|
3864
4030
|
program.command("diff").description("Show changes in a deployment").argument("<id>", "Deployment ID").option("--format <format>", "Output format: table or json", "table").option("--breaking", "Show only breaking changes").action(diffCommand);
|
|
3865
4031
|
program.command("annotate").description("Annotate deployment changes with reasoning").argument("<id>", "Deployment ID").option("--data <json>", "Annotations JSON").action(annotateCommand);
|
|
@@ -3870,6 +4036,11 @@ const keys = program.command("keys").description("Manage API keys for the Bonnar
|
|
|
3870
4036
|
keys.command("list").description("List all API keys for your organization").action(keysListCommand);
|
|
3871
4037
|
keys.command("create").description("Create a new API key").requiredOption("--name <name>", "Key name (e.g. 'Production SDK')").requiredOption("--type <type>", "Key type: publishable or secret").action(keysCreateCommand);
|
|
3872
4038
|
keys.command("revoke").description("Revoke an API key by name or prefix").argument("<name-or-prefix>", "Key name or key prefix to revoke").action(keysRevokeCommand);
|
|
4039
|
+
const dashboard = program.command("dashboard").description("Manage hosted HTML dashboards");
|
|
4040
|
+
dashboard.command("deploy").description("Deploy an HTML file as a hosted dashboard").argument("<file>", "Path to HTML file").option("--slug <slug>", "Dashboard slug (defaults to filename)").option("--title <title>", "Dashboard title (defaults to <title> tag or slug)").action(dashboardDeployCommand);
|
|
4041
|
+
dashboard.command("list").description("List deployed dashboards").action(dashboardListCommand);
|
|
4042
|
+
dashboard.command("remove").description("Remove a deployed dashboard").argument("<slug>", "Dashboard slug to remove").option("--force", "Skip confirmation prompt").action(dashboardRemoveCommand);
|
|
4043
|
+
dashboard.command("open").description("Open a deployed dashboard in the browser").argument("<slug>", "Dashboard slug to open").action(dashboardOpenCommand);
|
|
3873
4044
|
const metabase = program.command("metabase").description("Connect to and explore Metabase content");
|
|
3874
4045
|
metabase.command("connect").description("Configure Metabase API connection").option("--url <url>", "Metabase instance URL").option("--api-key <key>", "Metabase API key").option("--force", "Overwrite existing configuration").action(metabaseConnectCommand);
|
|
3875
4046
|
metabase.command("explore").description("Browse Metabase databases, collections, cards, and dashboards").argument("[resource]", "databases, collections, cards, dashboards, card, dashboard, database, table, collection").argument("[id]", "Resource ID (e.g. card <id>, dashboard <id>, database <id>, table <id>, collection <id>)").action(metabaseExploreCommand);
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# ApexCharts + Bonnard SDK
|
|
2
|
+
|
|
3
|
+
> Build HTML dashboards with ApexCharts and the Bonnard SDK. No build step required.
|
|
4
|
+
|
|
5
|
+
ApexCharts has the best visual defaults out of the box — no configuration needed for tooltips, responsive behavior, or dark mode. SVG-based rendering produces sharp visuals at any resolution. Moderate payload (~130KB gzip).
|
|
6
|
+
|
|
7
|
+
## Starter template
|
|
8
|
+
|
|
9
|
+
Copy this complete HTML file as a starting point. Replace `bon_pk_YOUR_KEY_HERE` with your publishable API key, and update the view/measure/dimension names to match your schema.
|
|
10
|
+
|
|
11
|
+
Use `explore()` to discover available views and fields — see [sdk.query-reference](sdk.query-reference).
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<!DOCTYPE html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="UTF-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
<title>Dashboard</title>
|
|
20
|
+
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3/dist/apexcharts.min.js"></script>
|
|
21
|
+
<script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script>
|
|
22
|
+
<style>
|
|
23
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
24
|
+
body {
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
26
|
+
background: #09090b; color: #fafafa; padding: 24px;
|
|
27
|
+
}
|
|
28
|
+
h1 { font-size: 24px; font-weight: 600; margin-bottom: 24px; }
|
|
29
|
+
.error { color: #ef4444; background: #1c0a0a; padding: 12px; border-radius: 8px; margin-bottom: 16px; display: none; }
|
|
30
|
+
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
|
31
|
+
.kpi { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; }
|
|
32
|
+
.kpi-label { font-size: 14px; color: #a1a1aa; margin-bottom: 8px; }
|
|
33
|
+
.kpi-value { font-size: 32px; font-weight: 600; }
|
|
34
|
+
.kpi-value.loading { color: #3f3f46; }
|
|
35
|
+
.charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px; }
|
|
36
|
+
.chart-card { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; }
|
|
37
|
+
.chart-title { font-size: 16px; font-weight: 500; margin-bottom: 16px; }
|
|
38
|
+
.chart-container { height: 300px; }
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<h1>Dashboard</h1>
|
|
43
|
+
<div id="error" class="error"></div>
|
|
44
|
+
|
|
45
|
+
<div class="kpis">
|
|
46
|
+
<div class="kpi">
|
|
47
|
+
<div class="kpi-label">Revenue</div>
|
|
48
|
+
<div class="kpi-value loading" id="kpi-revenue">--</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="kpi">
|
|
51
|
+
<div class="kpi-label">Orders</div>
|
|
52
|
+
<div class="kpi-value loading" id="kpi-orders">--</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="kpi">
|
|
55
|
+
<div class="kpi-label">Avg Value</div>
|
|
56
|
+
<div class="kpi-value loading" id="kpi-avg">--</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="charts">
|
|
61
|
+
<div class="chart-card">
|
|
62
|
+
<div class="chart-title">Revenue by City</div>
|
|
63
|
+
<div class="chart-container" id="bar-chart"></div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="chart-card">
|
|
66
|
+
<div class="chart-title">Revenue Trend</div>
|
|
67
|
+
<div class="chart-container" id="line-chart"></div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
const bon = Bonnard.createClient({
|
|
73
|
+
apiKey: 'bon_pk_YOUR_KEY_HERE',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// --- Helpers ---
|
|
77
|
+
function showError(msg) {
|
|
78
|
+
const el = document.getElementById('error');
|
|
79
|
+
el.textContent = msg;
|
|
80
|
+
el.style.display = 'block';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatNumber(v) {
|
|
84
|
+
return new Intl.NumberFormat().format(v);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatCurrency(v) {
|
|
88
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(v);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ApexCharts dark mode defaults
|
|
92
|
+
const darkTheme = {
|
|
93
|
+
chart: { background: 'transparent', foreColor: '#a1a1aa' },
|
|
94
|
+
theme: { mode: 'dark' },
|
|
95
|
+
grid: { borderColor: '#27272a' },
|
|
96
|
+
tooltip: { theme: 'dark' },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// --- Load data ---
|
|
100
|
+
(async () => {
|
|
101
|
+
try {
|
|
102
|
+
// KPIs
|
|
103
|
+
const kpis = await bon.query({
|
|
104
|
+
measures: ['orders.revenue', 'orders.count', 'orders.avg_value'],
|
|
105
|
+
});
|
|
106
|
+
if (kpis.data.length > 0) {
|
|
107
|
+
const row = kpis.data[0];
|
|
108
|
+
document.getElementById('kpi-revenue').textContent = formatCurrency(row['orders.revenue']);
|
|
109
|
+
document.getElementById('kpi-revenue').classList.remove('loading');
|
|
110
|
+
document.getElementById('kpi-orders').textContent = formatNumber(row['orders.count']);
|
|
111
|
+
document.getElementById('kpi-orders').classList.remove('loading');
|
|
112
|
+
document.getElementById('kpi-avg').textContent = formatCurrency(row['orders.avg_value']);
|
|
113
|
+
document.getElementById('kpi-avg').classList.remove('loading');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Bar chart — revenue by city
|
|
117
|
+
const byCity = await bon.query({
|
|
118
|
+
measures: ['orders.revenue'],
|
|
119
|
+
dimensions: ['orders.city'],
|
|
120
|
+
orderBy: { 'orders.revenue': 'desc' },
|
|
121
|
+
limit: 10,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
new ApexCharts(document.getElementById('bar-chart'), {
|
|
125
|
+
...darkTheme,
|
|
126
|
+
chart: { ...darkTheme.chart, type: 'bar', height: 300 },
|
|
127
|
+
series: [{ name: 'Revenue', data: byCity.data.map(d => d['orders.revenue']) }],
|
|
128
|
+
xaxis: { categories: byCity.data.map(d => d['orders.city']) },
|
|
129
|
+
yaxis: { labels: { formatter: v => formatCurrency(v) } },
|
|
130
|
+
plotOptions: { bar: { borderRadius: 4, columnWidth: '60%' } },
|
|
131
|
+
colors: ['#3b82f6'],
|
|
132
|
+
dataLabels: { enabled: false },
|
|
133
|
+
}).render();
|
|
134
|
+
|
|
135
|
+
// Line chart — revenue trend
|
|
136
|
+
const trend = await bon.query({
|
|
137
|
+
measures: ['orders.revenue'],
|
|
138
|
+
timeDimension: {
|
|
139
|
+
dimension: 'orders.created_at',
|
|
140
|
+
granularity: 'month',
|
|
141
|
+
dateRange: 'last 12 months',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
new ApexCharts(document.getElementById('line-chart'), {
|
|
146
|
+
...darkTheme,
|
|
147
|
+
chart: { ...darkTheme.chart, type: 'area', height: 300 },
|
|
148
|
+
series: [{ name: 'Revenue', data: trend.data.map(d => d['orders.revenue']) }],
|
|
149
|
+
xaxis: {
|
|
150
|
+
categories: trend.data.map(d => {
|
|
151
|
+
const date = new Date(d['orders.created_at']);
|
|
152
|
+
return date.toLocaleDateString('en-US', { month: 'short', year: '2-digit' });
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
yaxis: { labels: { formatter: v => formatCurrency(v) } },
|
|
156
|
+
colors: ['#3b82f6'],
|
|
157
|
+
fill: { type: 'gradient', gradient: { opacityFrom: 0.3, opacityTo: 0.05 } },
|
|
158
|
+
stroke: { curve: 'smooth', width: 2 },
|
|
159
|
+
dataLabels: { enabled: false },
|
|
160
|
+
}).render();
|
|
161
|
+
} catch (err) {
|
|
162
|
+
showError('Failed to load dashboard: ' + err.message);
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
</script>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Chart types
|
|
171
|
+
|
|
172
|
+
### Bar chart
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
new ApexCharts(el, {
|
|
176
|
+
chart: { type: 'bar', height: 300 },
|
|
177
|
+
series: [{ name: 'Revenue', data: data.map(d => d['view.measure']) }],
|
|
178
|
+
xaxis: { categories: data.map(d => d['view.dimension']) },
|
|
179
|
+
plotOptions: { bar: { borderRadius: 4 } },
|
|
180
|
+
colors: ['#3b82f6'],
|
|
181
|
+
}).render();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Horizontal bar chart
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
new ApexCharts(el, {
|
|
188
|
+
chart: { type: 'bar', height: 300 },
|
|
189
|
+
series: [{ name: 'Revenue', data: data.map(d => d['view.measure']) }],
|
|
190
|
+
xaxis: { categories: data.map(d => d['view.dimension']) },
|
|
191
|
+
plotOptions: { bar: { horizontal: true, borderRadius: 4 } },
|
|
192
|
+
}).render();
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Line chart
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
new ApexCharts(el, {
|
|
199
|
+
chart: { type: 'line', height: 300 },
|
|
200
|
+
series: [{ name: 'Revenue', data: values }],
|
|
201
|
+
xaxis: { categories: labels },
|
|
202
|
+
stroke: { curve: 'smooth', width: 2 },
|
|
203
|
+
}).render();
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Area chart
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
new ApexCharts(el, {
|
|
210
|
+
chart: { type: 'area', height: 300 },
|
|
211
|
+
series: [{ name: 'Revenue', data: values }],
|
|
212
|
+
xaxis: { categories: labels },
|
|
213
|
+
fill: { type: 'gradient', gradient: { opacityFrom: 0.3, opacityTo: 0.05 } },
|
|
214
|
+
stroke: { curve: 'smooth', width: 2 },
|
|
215
|
+
}).render();
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Pie / donut chart
|
|
219
|
+
|
|
220
|
+
```javascript
|
|
221
|
+
new ApexCharts(el, {
|
|
222
|
+
chart: { type: 'donut', height: 300 }, // or 'pie'
|
|
223
|
+
series: data.map(d => d['view.measure']),
|
|
224
|
+
labels: data.map(d => d['view.dimension']),
|
|
225
|
+
colors: ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6'],
|
|
226
|
+
}).render();
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Multi-series line chart
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
const cities = [...new Set(data.map(d => d['orders.city']))];
|
|
233
|
+
const dates = [...new Set(data.map(d => d['orders.created_at']))];
|
|
234
|
+
|
|
235
|
+
new ApexCharts(el, {
|
|
236
|
+
chart: { type: 'line', height: 300 },
|
|
237
|
+
series: cities.map(city => ({
|
|
238
|
+
name: city,
|
|
239
|
+
data: dates.map(date =>
|
|
240
|
+
data.find(d => d['orders.city'] === city && d['orders.created_at'] === date)?.['orders.revenue'] || 0
|
|
241
|
+
),
|
|
242
|
+
})),
|
|
243
|
+
xaxis: { categories: dates.map(d => new Date(d).toLocaleDateString()) },
|
|
244
|
+
stroke: { curve: 'smooth', width: 2 },
|
|
245
|
+
}).render();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Dark mode
|
|
249
|
+
|
|
250
|
+
ApexCharts has built-in dark mode support:
|
|
251
|
+
|
|
252
|
+
```javascript
|
|
253
|
+
const darkTheme = {
|
|
254
|
+
chart: { background: 'transparent', foreColor: '#a1a1aa' },
|
|
255
|
+
theme: { mode: 'dark' },
|
|
256
|
+
grid: { borderColor: '#27272a' },
|
|
257
|
+
tooltip: { theme: 'dark' },
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
new ApexCharts(el, {
|
|
261
|
+
...darkTheme,
|
|
262
|
+
chart: { ...darkTheme.chart, type: 'bar', height: 300 },
|
|
263
|
+
// ... rest of config
|
|
264
|
+
}).render();
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Color palette
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
const COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
|
|
271
|
+
|
|
272
|
+
// Apply globally:
|
|
273
|
+
colors: COLORS,
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## See also
|
|
277
|
+
|
|
278
|
+
- [sdk.browser](sdk.browser) — Browser / CDN quickstart
|
|
279
|
+
- [sdk.query-reference](sdk.query-reference) — Full query API
|
|
280
|
+
- [sdk.chartjs](sdk.chartjs) — Chart.js alternative (smallest payload)
|
|
281
|
+
- [sdk.echarts](sdk.echarts) — ECharts alternative (more chart types)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
> How to authenticate SDK requests — publishable keys for public dashboards, token exchange for multi-tenant apps.
|
|
4
|
+
|
|
5
|
+
## Publishable keys
|
|
6
|
+
|
|
7
|
+
Publishable keys (`bon_pk_...`) are safe to use in client-side code — HTML pages, browser apps, mobile apps. They grant read-only access to your org's semantic layer.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const bon = Bonnard.createClient({
|
|
11
|
+
apiKey: 'bon_pk_...',
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Create publishable keys in the Bonnard web app under **Settings > API Keys**.
|
|
16
|
+
|
|
17
|
+
**What publishable keys can do:**
|
|
18
|
+
- Query measures and dimensions
|
|
19
|
+
- Explore schema (views, fields)
|
|
20
|
+
|
|
21
|
+
**What they cannot do:**
|
|
22
|
+
- Modify data or schema
|
|
23
|
+
- Access other orgs' data
|
|
24
|
+
- Bypass governance policies (if configured at org level)
|
|
25
|
+
|
|
26
|
+
## Token exchange (multi-tenant)
|
|
27
|
+
|
|
28
|
+
For B2B apps where each customer should only see their own data, use **secret key token exchange**. Your server exchanges a secret key for a short-lived JWT with a security context, then your frontend queries with that token.
|
|
29
|
+
|
|
30
|
+
### Server-side: exchange secret key for scoped token
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// Your backend (Node.js, Python, etc.)
|
|
34
|
+
const res = await fetch('https://app.bonnard.dev/api/sdk/token', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`, // bon_sk_...
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
security_context: {
|
|
42
|
+
tenant_id: currentCustomer.id, // your tenant identifier
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { token } = await res.json();
|
|
48
|
+
// Pass this token to your frontend
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Client-side: query with scoped token
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
const bon = Bonnard.createClient({
|
|
55
|
+
fetchToken: async () => {
|
|
56
|
+
const res = await fetch('/my-backend/bonnard-token');
|
|
57
|
+
const { token } = await res.json();
|
|
58
|
+
return token;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const { data } = await bon.query({
|
|
63
|
+
measures: ['orders.revenue'],
|
|
64
|
+
dimensions: ['orders.status'],
|
|
65
|
+
});
|
|
66
|
+
// Only returns rows matching the tenant's security context
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### How token refresh works
|
|
70
|
+
|
|
71
|
+
The SDK automatically:
|
|
72
|
+
1. Calls `fetchToken()` on the first query
|
|
73
|
+
2. Caches the returned JWT
|
|
74
|
+
3. Parses the JWT `exp` claim
|
|
75
|
+
4. Refreshes 60 seconds before expiry by calling `fetchToken()` again
|
|
76
|
+
|
|
77
|
+
You don't need to manage token lifecycle — just provide the `fetchToken` callback.
|
|
78
|
+
|
|
79
|
+
### Security context and governance
|
|
80
|
+
|
|
81
|
+
The `security_context` object you pass during token exchange becomes available in your Cube models as `{securityContext.attrs.*}`. Use it in access policies to enforce row-level security:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
# In your Cube view definition
|
|
85
|
+
access_policy:
|
|
86
|
+
- role: "*"
|
|
87
|
+
conditions:
|
|
88
|
+
- sql: "{TABLE}.tenant_id = '{securityContext.attrs.tenant_id}'"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
See [security-context](security-context) for the full governance setup guide.
|
|
92
|
+
|
|
93
|
+
## Browser HTML with token exchange
|
|
94
|
+
|
|
95
|
+
For HTML dashboards that need multi-tenant auth, your page fetches a token from your backend:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script>
|
|
99
|
+
<script>
|
|
100
|
+
const bon = Bonnard.createClient({
|
|
101
|
+
fetchToken: async () => {
|
|
102
|
+
const res = await fetch('/api/bonnard-token');
|
|
103
|
+
const { token } = await res.json();
|
|
104
|
+
return token;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
(async () => {
|
|
109
|
+
const { data } = await bon.query({
|
|
110
|
+
measures: ['orders.revenue'],
|
|
111
|
+
});
|
|
112
|
+
// Data is scoped to the authenticated tenant
|
|
113
|
+
})();
|
|
114
|
+
</script>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## When to use which
|
|
118
|
+
|
|
119
|
+
| Scenario | Auth method | Key type |
|
|
120
|
+
|----------|------------|----------|
|
|
121
|
+
| Internal dashboard (your team) | Publishable key | `bon_pk_...` |
|
|
122
|
+
| Public dashboard (anyone can view) | Publishable key | `bon_pk_...` |
|
|
123
|
+
| Embedded analytics (customer sees their data only) | Token exchange | `bon_sk_...` → JWT |
|
|
124
|
+
| Server-side data pipeline | Secret key directly | `bon_sk_...` |
|
|
125
|
+
|
|
126
|
+
## See also
|
|
127
|
+
|
|
128
|
+
- [sdk.browser](sdk.browser) — Browser / CDN quickstart
|
|
129
|
+
- [sdk.query-reference](sdk.query-reference) — Full query API
|
|
130
|
+
- [security-context](security-context) — Row-level security setup
|