@bonnard/cli 0.2.6 → 0.2.8
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 +10 -8
- package/dist/bin/bon.mjs +142 -65
- package/dist/docs/topics/cli.md +16 -0
- package/dist/docs/topics/dashboards.components.md +102 -12
- package/dist/docs/topics/dashboards.examples.md +163 -6
- package/dist/docs/topics/dashboards.inputs.md +179 -0
- package/dist/docs/topics/dashboards.md +39 -5
- package/dist/docs/topics/dashboards.queries.md +7 -5
- package/dist/docs/topics/views.md +1 -1
- package/dist/templates/claude/skills/bonnard-design-guide/SKILL.md +5 -4
- package/dist/templates/cursor/rules/bonnard-design-guide.mdc +5 -4
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -2,24 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
The Bonnard CLI (`bon`) takes you from zero to a deployed semantic layer in minutes. Define metrics in YAML, validate locally, deploy, and query — from your terminal or AI coding agent.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g @bonnard/cli
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Requires Node.js 20+.
|
|
5
|
+
**Open source** — [view source on GitHub](https://github.com/meal-inc/bonnard-cli)
|
|
12
6
|
|
|
13
7
|
## Quick start
|
|
14
8
|
|
|
15
9
|
```bash
|
|
16
|
-
|
|
10
|
+
npx @bonnard/cli init # Create project structure + agent templates
|
|
17
11
|
bon datasource add --demo # Add demo dataset (no warehouse needed)
|
|
18
12
|
bon validate # Check syntax
|
|
19
13
|
bon login # Authenticate with Bonnard
|
|
20
14
|
bon deploy -m "Initial deploy" # Deploy to Bonnard
|
|
21
15
|
```
|
|
22
16
|
|
|
17
|
+
No install needed — `npx` runs the CLI directly. Or install globally for shorter commands:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g @bonnard/cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Node.js 20+.
|
|
24
|
+
|
|
23
25
|
## Commands
|
|
24
26
|
|
|
25
27
|
| Command | Description |
|
package/dist/bin/bon.mjs
CHANGED
|
@@ -2698,11 +2698,11 @@ async function showOverview(client) {
|
|
|
2698
2698
|
console.log(pc.dim(" bon metabase explore collections"));
|
|
2699
2699
|
console.log(pc.dim(" bon metabase explore cards"));
|
|
2700
2700
|
console.log(pc.dim(" bon metabase explore dashboards"));
|
|
2701
|
-
console.log(pc.dim(" bon metabase explore card <id>"));
|
|
2702
|
-
console.log(pc.dim(" bon metabase explore dashboard <id>"));
|
|
2703
|
-
console.log(pc.dim(" bon metabase explore database <id>"));
|
|
2704
|
-
console.log(pc.dim(" bon metabase explore table <id>"));
|
|
2705
|
-
console.log(pc.dim(" bon metabase explore collection <id>"));
|
|
2701
|
+
console.log(pc.dim(" bon metabase explore card <id-or-name>"));
|
|
2702
|
+
console.log(pc.dim(" bon metabase explore dashboard <id-or-name>"));
|
|
2703
|
+
console.log(pc.dim(" bon metabase explore database <id-or-name>"));
|
|
2704
|
+
console.log(pc.dim(" bon metabase explore table <id-or-name>"));
|
|
2705
|
+
console.log(pc.dim(" bon metabase explore collection <id-or-name>"));
|
|
2706
2706
|
}
|
|
2707
2707
|
async function showDatabases(client) {
|
|
2708
2708
|
const databases = await client.getDatabases();
|
|
@@ -2778,7 +2778,7 @@ async function showCards(client) {
|
|
|
2778
2778
|
console.log();
|
|
2779
2779
|
}
|
|
2780
2780
|
if (models.length === 0 && metrics.length === 0 && questions.length === 0) console.log(pc.dim(" No cards found."));
|
|
2781
|
-
console.log(pc.dim("View details: bon metabase explore card <id>"));
|
|
2781
|
+
console.log(pc.dim("View details: bon metabase explore card <id-or-name>"));
|
|
2782
2782
|
}
|
|
2783
2783
|
async function showCardDetail(client, id) {
|
|
2784
2784
|
const card = await client.getCard(id);
|
|
@@ -2835,7 +2835,7 @@ async function showDashboards(client) {
|
|
|
2835
2835
|
console.log(` ${pc.dim(padColumn("ID", 6))}${pc.dim("NAME")}`);
|
|
2836
2836
|
for (const d of active) console.log(` ${padColumn(String(d.id), 6)}${d.name}`);
|
|
2837
2837
|
console.log();
|
|
2838
|
-
console.log(pc.dim("View details: bon metabase explore dashboard <id>"));
|
|
2838
|
+
console.log(pc.dim("View details: bon metabase explore dashboard <id-or-name>"));
|
|
2839
2839
|
}
|
|
2840
2840
|
async function showDashboardDetail(client, id) {
|
|
2841
2841
|
const [dashboard, allCards] = await Promise.all([client.getDashboard(id), client.getCards()]);
|
|
@@ -2895,7 +2895,7 @@ async function showDatabaseDetail(client, id) {
|
|
|
2895
2895
|
}
|
|
2896
2896
|
console.log();
|
|
2897
2897
|
}
|
|
2898
|
-
console.log(pc.dim("View table fields: bon metabase explore table <id>"));
|
|
2898
|
+
console.log(pc.dim("View table fields: bon metabase explore table <id-or-name>"));
|
|
2899
2899
|
}
|
|
2900
2900
|
function classifyFieldType(field) {
|
|
2901
2901
|
const bt = field.base_type || "";
|
|
@@ -3008,8 +3008,105 @@ async function showCollectionDetail(client, id) {
|
|
|
3008
3008
|
console.log();
|
|
3009
3009
|
}
|
|
3010
3010
|
if (cardItems.length === 0 && dashboardItems.length === 0) console.log(pc.dim(" No items in this collection."));
|
|
3011
|
-
console.log(pc.dim("View card SQL: bon metabase explore card <id>"));
|
|
3012
|
-
console.log(pc.dim("View dashboard: bon metabase explore dashboard <id>"));
|
|
3011
|
+
console.log(pc.dim("View card SQL: bon metabase explore card <id-or-name>"));
|
|
3012
|
+
console.log(pc.dim("View dashboard: bon metabase explore dashboard <id-or-name>"));
|
|
3013
|
+
}
|
|
3014
|
+
function isNumericId(value) {
|
|
3015
|
+
return /^\d+$/.test(value);
|
|
3016
|
+
}
|
|
3017
|
+
function showDisambiguation(resource, matches) {
|
|
3018
|
+
console.error(pc.yellow(`Multiple ${resource}s match that name:\n`));
|
|
3019
|
+
for (const m of matches) console.log(` ${padColumn(String(m.id), 8)}${m.label}`);
|
|
3020
|
+
console.log();
|
|
3021
|
+
console.log(pc.dim(`Use the numeric ID to be specific: bon metabase explore ${resource} <id>`));
|
|
3022
|
+
process.exit(1);
|
|
3023
|
+
}
|
|
3024
|
+
async function resolveCardId(client, input) {
|
|
3025
|
+
if (isNumericId(input)) return parseInt(input, 10);
|
|
3026
|
+
const cards = await client.getCards();
|
|
3027
|
+
const needle = input.toLowerCase();
|
|
3028
|
+
const matches = cards.filter((c) => c.name?.toLowerCase().includes(needle));
|
|
3029
|
+
if (matches.length === 0) {
|
|
3030
|
+
console.error(pc.red(`No card found matching "${input}"`));
|
|
3031
|
+
process.exit(1);
|
|
3032
|
+
}
|
|
3033
|
+
if (matches.length === 1) return matches[0].id;
|
|
3034
|
+
showDisambiguation("card", matches.map((c) => ({
|
|
3035
|
+
id: c.id,
|
|
3036
|
+
label: c.name
|
|
3037
|
+
})));
|
|
3038
|
+
}
|
|
3039
|
+
async function resolveDashboardId(client, input) {
|
|
3040
|
+
if (isNumericId(input)) return parseInt(input, 10);
|
|
3041
|
+
const dashboards = await client.getDashboards();
|
|
3042
|
+
const needle = input.toLowerCase();
|
|
3043
|
+
const matches = dashboards.filter((d) => d.name?.toLowerCase().includes(needle));
|
|
3044
|
+
if (matches.length === 0) {
|
|
3045
|
+
console.error(pc.red(`No dashboard found matching "${input}"`));
|
|
3046
|
+
process.exit(1);
|
|
3047
|
+
}
|
|
3048
|
+
if (matches.length === 1) return matches[0].id;
|
|
3049
|
+
showDisambiguation("dashboard", matches.map((d) => ({
|
|
3050
|
+
id: d.id,
|
|
3051
|
+
label: d.name
|
|
3052
|
+
})));
|
|
3053
|
+
}
|
|
3054
|
+
async function resolveDatabaseId(client, input) {
|
|
3055
|
+
if (isNumericId(input)) return parseInt(input, 10);
|
|
3056
|
+
const databases = await client.getDatabases();
|
|
3057
|
+
const needle = input.toLowerCase();
|
|
3058
|
+
const matches = databases.filter((d) => d.name?.toLowerCase().includes(needle));
|
|
3059
|
+
if (matches.length === 0) {
|
|
3060
|
+
console.error(pc.red(`No database found matching "${input}"`));
|
|
3061
|
+
process.exit(1);
|
|
3062
|
+
}
|
|
3063
|
+
if (matches.length === 1) return matches[0].id;
|
|
3064
|
+
showDisambiguation("database", matches.map((d) => ({
|
|
3065
|
+
id: d.id,
|
|
3066
|
+
label: `${d.name} (${d.engine})`
|
|
3067
|
+
})));
|
|
3068
|
+
}
|
|
3069
|
+
async function resolveTableId(client, input) {
|
|
3070
|
+
if (isNumericId(input)) return parseInt(input, 10);
|
|
3071
|
+
const databases = await client.getDatabases();
|
|
3072
|
+
const needle = input.toLowerCase();
|
|
3073
|
+
const matches = [];
|
|
3074
|
+
for (const db of databases) {
|
|
3075
|
+
const meta = await client.getDatabaseMetadata(db.id);
|
|
3076
|
+
for (const t of meta.tables) {
|
|
3077
|
+
if (t.visibility_type === "hidden" || t.visibility_type === "retired") continue;
|
|
3078
|
+
if (t.name.toLowerCase().includes(needle)) matches.push({
|
|
3079
|
+
id: t.id,
|
|
3080
|
+
name: t.name,
|
|
3081
|
+
schema: t.schema,
|
|
3082
|
+
dbName: db.name
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
if (matches.length === 0) {
|
|
3087
|
+
console.error(pc.red(`No table found matching "${input}"`));
|
|
3088
|
+
process.exit(1);
|
|
3089
|
+
}
|
|
3090
|
+
if (matches.length === 1) return matches[0].id;
|
|
3091
|
+
showDisambiguation("table", matches.map((m) => ({
|
|
3092
|
+
id: m.id,
|
|
3093
|
+
label: `${m.dbName} / ${m.schema}.${m.name}`
|
|
3094
|
+
})));
|
|
3095
|
+
}
|
|
3096
|
+
async function resolveCollectionId(client, input) {
|
|
3097
|
+
if (isNumericId(input)) return parseInt(input, 10);
|
|
3098
|
+
const collections = await client.getCollections();
|
|
3099
|
+
const needle = input.toLowerCase();
|
|
3100
|
+
const matches = collections.filter((c) => typeof c.id === "number" && c.name?.toLowerCase().includes(needle));
|
|
3101
|
+
if (matches.length === 0) {
|
|
3102
|
+
console.error(pc.red(`No collection found matching "${input}"`));
|
|
3103
|
+
process.exit(1);
|
|
3104
|
+
}
|
|
3105
|
+
if (matches.length === 1) return matches[0].id;
|
|
3106
|
+
showDisambiguation("collection", matches.map((c) => ({
|
|
3107
|
+
id: c.id,
|
|
3108
|
+
label: c.name
|
|
3109
|
+
})));
|
|
3013
3110
|
}
|
|
3014
3111
|
const RESOURCES = [
|
|
3015
3112
|
"databases",
|
|
@@ -3047,71 +3144,41 @@ async function metabaseExploreCommand(resource, id) {
|
|
|
3047
3144
|
case "dashboards":
|
|
3048
3145
|
await showDashboards(client);
|
|
3049
3146
|
break;
|
|
3050
|
-
case "card":
|
|
3147
|
+
case "card":
|
|
3051
3148
|
if (!id) {
|
|
3052
|
-
console.error(pc.red("
|
|
3149
|
+
console.error(pc.red("Usage: bon metabase explore card <id-or-name>"));
|
|
3053
3150
|
process.exit(1);
|
|
3054
3151
|
}
|
|
3055
|
-
|
|
3056
|
-
if (isNaN(cardId)) {
|
|
3057
|
-
console.error(pc.red(`Invalid card ID: ${id}`));
|
|
3058
|
-
process.exit(1);
|
|
3059
|
-
}
|
|
3060
|
-
await showCardDetail(client, cardId);
|
|
3152
|
+
await showCardDetail(client, await resolveCardId(client, id));
|
|
3061
3153
|
break;
|
|
3062
|
-
|
|
3063
|
-
case "dashboard": {
|
|
3154
|
+
case "dashboard":
|
|
3064
3155
|
if (!id) {
|
|
3065
|
-
console.error(pc.red("
|
|
3156
|
+
console.error(pc.red("Usage: bon metabase explore dashboard <id-or-name>"));
|
|
3066
3157
|
process.exit(1);
|
|
3067
3158
|
}
|
|
3068
|
-
|
|
3069
|
-
if (isNaN(dashId)) {
|
|
3070
|
-
console.error(pc.red(`Invalid dashboard ID: ${id}`));
|
|
3071
|
-
process.exit(1);
|
|
3072
|
-
}
|
|
3073
|
-
await showDashboardDetail(client, dashId);
|
|
3159
|
+
await showDashboardDetail(client, await resolveDashboardId(client, id));
|
|
3074
3160
|
break;
|
|
3075
|
-
|
|
3076
|
-
case "database": {
|
|
3161
|
+
case "database":
|
|
3077
3162
|
if (!id) {
|
|
3078
|
-
console.error(pc.red("
|
|
3079
|
-
process.exit(1);
|
|
3080
|
-
}
|
|
3081
|
-
const dbId = parseInt(id, 10);
|
|
3082
|
-
if (isNaN(dbId)) {
|
|
3083
|
-
console.error(pc.red(`Invalid database ID: ${id}`));
|
|
3163
|
+
console.error(pc.red("Usage: bon metabase explore database <id-or-name>"));
|
|
3084
3164
|
process.exit(1);
|
|
3085
3165
|
}
|
|
3086
|
-
await showDatabaseDetail(client,
|
|
3166
|
+
await showDatabaseDetail(client, await resolveDatabaseId(client, id));
|
|
3087
3167
|
break;
|
|
3088
|
-
|
|
3089
|
-
case "table": {
|
|
3168
|
+
case "table":
|
|
3090
3169
|
if (!id) {
|
|
3091
|
-
console.error(pc.red("
|
|
3170
|
+
console.error(pc.red("Usage: bon metabase explore table <id-or-name>"));
|
|
3092
3171
|
process.exit(1);
|
|
3093
3172
|
}
|
|
3094
|
-
|
|
3095
|
-
if (isNaN(tableId)) {
|
|
3096
|
-
console.error(pc.red(`Invalid table ID: ${id}`));
|
|
3097
|
-
process.exit(1);
|
|
3098
|
-
}
|
|
3099
|
-
await showTableDetail(client, tableId);
|
|
3173
|
+
await showTableDetail(client, await resolveTableId(client, id));
|
|
3100
3174
|
break;
|
|
3101
|
-
|
|
3102
|
-
case "collection": {
|
|
3175
|
+
case "collection":
|
|
3103
3176
|
if (!id) {
|
|
3104
|
-
console.error(pc.red("
|
|
3177
|
+
console.error(pc.red("Usage: bon metabase explore collection <id-or-name>"));
|
|
3105
3178
|
process.exit(1);
|
|
3106
3179
|
}
|
|
3107
|
-
|
|
3108
|
-
if (isNaN(colId)) {
|
|
3109
|
-
console.error(pc.red(`Invalid collection ID: ${id}`));
|
|
3110
|
-
process.exit(1);
|
|
3111
|
-
}
|
|
3112
|
-
await showCollectionDetail(client, colId);
|
|
3180
|
+
await showCollectionDetail(client, await resolveCollectionId(client, id));
|
|
3113
3181
|
break;
|
|
3114
|
-
}
|
|
3115
3182
|
}
|
|
3116
3183
|
} catch (err) {
|
|
3117
3184
|
if (err instanceof MetabaseApiError) {
|
|
@@ -3454,10 +3521,10 @@ function buildReport(data) {
|
|
|
3454
3521
|
report += `5. **Collection Structure** → Map collections to views (one view per business domain)\n`;
|
|
3455
3522
|
report += `6. **Table Inventory** → Use field classification (dims/measures/time) to build each cube\n\n`;
|
|
3456
3523
|
report += `Drill deeper with:\n`;
|
|
3457
|
-
report += `- \`bon metabase explore table <id>\` — field types and classification\n`;
|
|
3458
|
-
report += `- \`bon metabase explore card <id>\` — SQL and columns\n`;
|
|
3459
|
-
report += `- \`bon metabase explore collection <id>\` — cards in a collection\n`;
|
|
3460
|
-
report += `- \`bon metabase explore database <id>\` — schemas and tables\n\n`;
|
|
3524
|
+
report += `- \`bon metabase explore table <id-or-name>\` — field types and classification\n`;
|
|
3525
|
+
report += `- \`bon metabase explore card <id-or-name>\` — SQL and columns\n`;
|
|
3526
|
+
report += `- \`bon metabase explore collection <id-or-name>\` — cards in a collection\n`;
|
|
3527
|
+
report += `- \`bon metabase explore database <id-or-name>\` — schemas and tables\n\n`;
|
|
3461
3528
|
report += `## Summary\n\n`;
|
|
3462
3529
|
report += `| Metric | Count |\n|--------|-------|\n`;
|
|
3463
3530
|
report += `| Databases | ${databases.length} |\n`;
|
|
@@ -3497,7 +3564,7 @@ function buildReport(data) {
|
|
|
3497
3564
|
report += "```\n\n";
|
|
3498
3565
|
report += `## Top ${topCards.length} Cards by Activity\n\n`;
|
|
3499
3566
|
report += `Ranked by view count, weighted by recency. Cards not used in the last ${INACTIVE_MONTHS} months are penalized 90%.\n`;
|
|
3500
|
-
report += `Use \`bon metabase explore card <id>\` to view SQL and column details for any card.\n\n`;
|
|
3567
|
+
report += `Use \`bon metabase explore card <id-or-name>\` to view SQL and column details for any card.\n\n`;
|
|
3501
3568
|
report += `| Rank | ID | Views | Last Used | Active | Pattern | Type | Display | Collection | Name |\n`;
|
|
3502
3569
|
report += `|------|----|-------|-----------|--------|---------|------|---------|------------|------|\n`;
|
|
3503
3570
|
for (let i = 0; i < topCards.length; i++) {
|
|
@@ -3555,8 +3622,18 @@ function buildReport(data) {
|
|
|
3555
3622
|
const sortedRefs = Array.from(globalTableRefs.entries()).sort((a, b) => b[1] - a[1]).slice(0, 20);
|
|
3556
3623
|
report += `## Most Referenced Tables (from SQL)\n\n`;
|
|
3557
3624
|
report += `Tables most frequently referenced in FROM/JOIN clauses across all cards.\n\n`;
|
|
3558
|
-
|
|
3559
|
-
for (const
|
|
3625
|
+
const tableIdByRef = /* @__PURE__ */ new Map();
|
|
3626
|
+
for (const db of databases) for (const t of db.tables) {
|
|
3627
|
+
const qualified = `${t.schema}.${t.name}`.toLowerCase();
|
|
3628
|
+
const unqualified = t.name.toLowerCase();
|
|
3629
|
+
if (!tableIdByRef.has(qualified)) tableIdByRef.set(qualified, t.id);
|
|
3630
|
+
if (!tableIdByRef.has(unqualified)) tableIdByRef.set(unqualified, t.id);
|
|
3631
|
+
}
|
|
3632
|
+
report += `| ID | Table | References |\n|------|-------|------------|\n`;
|
|
3633
|
+
for (const [table, count] of sortedRefs) {
|
|
3634
|
+
const tid = tableIdByRef.get(table);
|
|
3635
|
+
report += `| ${tid ?? "—"} | ${table} | ${count} |\n`;
|
|
3636
|
+
}
|
|
3560
3637
|
report += `\n`;
|
|
3561
3638
|
}
|
|
3562
3639
|
const fieldIdLookup = /* @__PURE__ */ new Map();
|
|
@@ -3656,9 +3733,9 @@ function buildReport(data) {
|
|
|
3656
3733
|
report += `### ${db.name} / ${schema} (${referenced.length} referenced`;
|
|
3657
3734
|
if (unreferenced > 0) report += `, ${unreferenced} unreferenced`;
|
|
3658
3735
|
report += `)\n\n`;
|
|
3659
|
-
report += `| Table | Fields | Dims | Measures | Time | Refs |\n`;
|
|
3660
|
-
report +=
|
|
3661
|
-
for (const s of referenced) report += `| ${s.name} | ${s.fieldCount} | ${s.dimensions} | ${s.measures} | ${s.timeDimensions} | ${s.refCount} |\n`;
|
|
3736
|
+
report += `| ID | Table | Fields | Dims | Measures | Time | Refs |\n`;
|
|
3737
|
+
report += `|------|-------|--------|------|----------|------|------|\n`;
|
|
3738
|
+
for (const s of referenced) report += `| ${s.id} | ${s.name} | ${s.fieldCount} | ${s.dimensions} | ${s.measures} | ${s.timeDimensions} | ${s.refCount} |\n`;
|
|
3662
3739
|
if (unreferenced > 0) skippedTables += unreferenced;
|
|
3663
3740
|
report += `\n`;
|
|
3664
3741
|
}
|
package/dist/docs/topics/cli.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
The Bonnard CLI (`bon`) takes you from zero to a deployed semantic layer in minutes. Initialize a project, connect your warehouse, define metrics in YAML, validate locally, and deploy — all from your terminal or your AI coding agent.
|
|
6
6
|
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Run directly with `npx` — no install needed:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @bonnard/cli init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install globally for shorter commands:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @bonnard/cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Node.js 20+.
|
|
22
|
+
|
|
7
23
|
## Agent-ready from the start
|
|
8
24
|
|
|
9
25
|
`bon init` generates context files for your AI coding tools automatically:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Components
|
|
2
2
|
|
|
3
3
|
> Chart and display components for rendering query results in dashboards.
|
|
4
4
|
|
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
Components are self-closing HTML-style tags that render query results as charts, tables, or KPI cards. Each component takes a `data` prop referencing a named query.
|
|
8
8
|
|
|
9
|
+
Choose the component that best fits your data:
|
|
10
|
+
|
|
11
|
+
- **BigValue** — single KPI number (total revenue, order count)
|
|
12
|
+
- **LineChart** — trends over time
|
|
13
|
+
- **BarChart** — comparing categories (vertical or horizontal)
|
|
14
|
+
- **AreaChart** — cumulative or stacked trends
|
|
15
|
+
- **PieChart** — proportional breakdown (best with 5-7 slices)
|
|
16
|
+
- **DataTable** — detailed rows for drilling into data
|
|
17
|
+
|
|
9
18
|
## Syntax
|
|
10
19
|
|
|
11
20
|
```markdown
|
|
@@ -32,53 +41,68 @@ Displays a single KPI metric as a large number.
|
|
|
32
41
|
| `data` | query ref | Yes | Query name (should return a single row) |
|
|
33
42
|
| `value` | string | Yes | Measure field name to display |
|
|
34
43
|
| `title` | string | No | Label above the value |
|
|
44
|
+
| `fmt` | string | No | Format preset or Excel code (e.g. `fmt="eur2"`, `fmt="$#,##0.00"`) |
|
|
35
45
|
|
|
36
46
|
### LineChart
|
|
37
47
|
|
|
38
|
-
Renders a line chart, typically for time series.
|
|
48
|
+
Renders a line chart, typically for time series. Supports multiple y columns and series splitting.
|
|
39
49
|
|
|
40
50
|
```markdown
|
|
41
51
|
<LineChart data={monthly_revenue} x="created_at" y="total_revenue" title="Revenue Trend" />
|
|
52
|
+
<LineChart data={trend} x="date" y="revenue,cases" />
|
|
53
|
+
<LineChart data={revenue_by_type} x="created_at" y="total_revenue" series="type" />
|
|
42
54
|
```
|
|
43
55
|
|
|
44
56
|
| Prop | Type | Required | Description |
|
|
45
57
|
|------|------|----------|-------------|
|
|
46
58
|
| `data` | query ref | Yes | Query name |
|
|
47
59
|
| `x` | string | Yes | Field for x-axis (typically a time dimension) |
|
|
48
|
-
| `y` | string | Yes | Field for y-axis (
|
|
60
|
+
| `y` | string | Yes | Field(s) for y-axis. Comma-separated for multiple (e.g. `y="revenue,cases"`) |
|
|
49
61
|
| `title` | string | No | Chart title |
|
|
62
|
+
| `series` | string | No | Column to split data into separate colored lines |
|
|
63
|
+
| `type` | string | No | `"stacked"` for stacked lines (default: no stacking) |
|
|
64
|
+
| `yFmt` | string | No | Format preset or Excel code for tooltip values (e.g. `yFmt="eur2"`) |
|
|
50
65
|
|
|
51
66
|
### BarChart
|
|
52
67
|
|
|
53
|
-
Renders a vertical bar chart. Add `horizontal` for horizontal bars.
|
|
68
|
+
Renders a vertical bar chart. Add `horizontal` for horizontal bars. Supports multi-series with stacked or grouped display.
|
|
54
69
|
|
|
55
70
|
```markdown
|
|
56
71
|
<BarChart data={revenue_by_city} x="city" y="total_revenue" />
|
|
57
72
|
<BarChart data={revenue_by_city} x="city" y="total_revenue" horizontal />
|
|
73
|
+
<BarChart data={revenue_by_type} x="month" y="total_revenue" series="type" />
|
|
74
|
+
<BarChart data={revenue_by_type} x="month" y="total_revenue" series="type" type="grouped" />
|
|
58
75
|
```
|
|
59
76
|
|
|
60
77
|
| Prop | Type | Required | Description |
|
|
61
78
|
|------|------|----------|-------------|
|
|
62
79
|
| `data` | query ref | Yes | Query name |
|
|
63
80
|
| `x` | string | Yes | Field for category axis |
|
|
64
|
-
| `y` | string | Yes | Field for value axis |
|
|
81
|
+
| `y` | string | Yes | Field(s) for value axis. Comma-separated for multiple (e.g. `y="revenue,cases"`) |
|
|
65
82
|
| `title` | string | No | Chart title |
|
|
66
83
|
| `horizontal` | boolean | No | Render as horizontal bar chart |
|
|
84
|
+
| `series` | string | No | Column to split data into separate colored bars |
|
|
85
|
+
| `type` | string | No | `"stacked"` (default) or `"grouped"` for multi-series display |
|
|
86
|
+
| `yFmt` | string | No | Format preset or Excel code for tooltip values (e.g. `yFmt="usd"`) |
|
|
67
87
|
|
|
68
88
|
### AreaChart
|
|
69
89
|
|
|
70
|
-
Renders a filled area chart.
|
|
90
|
+
Renders a filled area chart. Supports series splitting and stacked areas.
|
|
71
91
|
|
|
72
92
|
```markdown
|
|
73
93
|
<AreaChart data={monthly_revenue} x="created_at" y="total_revenue" />
|
|
94
|
+
<AreaChart data={revenue_by_source} x="created_at" y="total_revenue" series="source" type="stacked" />
|
|
74
95
|
```
|
|
75
96
|
|
|
76
97
|
| Prop | Type | Required | Description |
|
|
77
98
|
|------|------|----------|-------------|
|
|
78
99
|
| `data` | query ref | Yes | Query name |
|
|
79
100
|
| `x` | string | Yes | Field for x-axis |
|
|
80
|
-
| `y` | string | Yes | Field for y-axis |
|
|
101
|
+
| `y` | string | Yes | Field(s) for y-axis. Comma-separated for multiple (e.g. `y="revenue,cases"`) |
|
|
81
102
|
| `title` | string | No | Chart title |
|
|
103
|
+
| `series` | string | No | Column to split data into separate colored areas |
|
|
104
|
+
| `type` | string | No | `"stacked"` for stacked areas (default: no stacking) |
|
|
105
|
+
| `yFmt` | string | No | Format preset or Excel code for tooltip values (e.g. `yFmt="pct1"`) |
|
|
82
106
|
|
|
83
107
|
### PieChart
|
|
84
108
|
|
|
@@ -97,11 +121,13 @@ Renders a pie/donut chart.
|
|
|
97
121
|
|
|
98
122
|
### DataTable
|
|
99
123
|
|
|
100
|
-
Renders query results as a table.
|
|
124
|
+
Renders query results as a sortable, paginated table. Click any column header to sort ascending/descending.
|
|
101
125
|
|
|
102
126
|
```markdown
|
|
103
127
|
<DataTable data={top_products} />
|
|
104
128
|
<DataTable data={top_products} columns="name,revenue,count" />
|
|
129
|
+
<DataTable data={top_products} rows="25" />
|
|
130
|
+
<DataTable data={top_products} rows="all" />
|
|
105
131
|
```
|
|
106
132
|
|
|
107
133
|
| Prop | Type | Required | Description |
|
|
@@ -109,8 +135,28 @@ Renders query results as a table.
|
|
|
109
135
|
| `data` | query ref | Yes | Query name |
|
|
110
136
|
| `columns` | string | No | Comma-separated list of columns to show (default: all) |
|
|
111
137
|
| `title` | string | No | Table title |
|
|
138
|
+
| `fmt` | string | No | Column format map: `fmt="revenue:eur2,date:shortdate"` |
|
|
139
|
+
| `rows` | string | No | Rows per page. Default `10`. Use `rows="all"` to disable pagination. |
|
|
140
|
+
|
|
141
|
+
**Sorting:** Click a column header to sort ascending. Click again to sort descending. Null values always sort to the end. Numbers sort numerically, strings sort case-insensitively.
|
|
142
|
+
|
|
143
|
+
**Formatting:** Numbers right-align with tabular figures. Dates auto-detect and won't wrap. Use `fmt` for explicit formatting per column.
|
|
144
|
+
|
|
145
|
+
## Layout
|
|
146
|
+
|
|
147
|
+
### Auto BigValue Grouping
|
|
148
|
+
|
|
149
|
+
Consecutive `<BigValue>` components are automatically wrapped in a responsive grid — no `<Grid>` tag needed:
|
|
150
|
+
|
|
151
|
+
```markdown
|
|
152
|
+
<BigValue data={total_revenue} value="total_revenue" title="Revenue" />
|
|
153
|
+
<BigValue data={order_count} value="count" title="Orders" />
|
|
154
|
+
<BigValue data={avg_order} value="avg_order_value" title="Avg Order" />
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This renders as a 3-column row. The grid auto-sizes up to 4 columns based on the number of consecutive BigValues. For more control, use an explicit `<Grid>` tag.
|
|
112
158
|
|
|
113
|
-
|
|
159
|
+
### Grid
|
|
114
160
|
|
|
115
161
|
Wrap components in a `<Grid>` tag to arrange them in columns:
|
|
116
162
|
|
|
@@ -126,12 +172,56 @@ Wrap components in a `<Grid>` tag to arrange them in columns:
|
|
|
126
172
|
|------|------|---------|-------------|
|
|
127
173
|
| `cols` | string | `"2"` | Number of columns in the grid |
|
|
128
174
|
|
|
175
|
+
## Formatting
|
|
176
|
+
|
|
177
|
+
Values are auto-formatted by default — numbers get locale grouping (1,234.56), dates display as "13 Jan 2025", and nulls show as "—". Override with named presets for common currencies and percentages, or use raw Excel format codes for full control.
|
|
178
|
+
|
|
179
|
+
### Format Presets
|
|
180
|
+
|
|
181
|
+
| Preset | Excel code | Example output |
|
|
182
|
+
|--------|-----------|---------------|
|
|
183
|
+
| `num0` | `#,##0` | 1,234 |
|
|
184
|
+
| `num1` | `#,##0.0` | 1,234.6 |
|
|
185
|
+
| `num2` | `#,##0.00` | 1,234.56 |
|
|
186
|
+
| `usd` | `$#,##0` | $1,234 |
|
|
187
|
+
| `usd2` | `$#,##0.00` | $1,234.56 |
|
|
188
|
+
| `eur` | `#,##0 "€"` | 1,234 € |
|
|
189
|
+
| `eur2` | `#,##0.00 "€"` | 1,234.56 € |
|
|
190
|
+
| `gbp` | `£#,##0` | £1,234 |
|
|
191
|
+
| `gbp2` | `£#,##0.00` | £1,234.56 |
|
|
192
|
+
| `chf` | `"CHF "#,##0` | CHF 1,234 |
|
|
193
|
+
| `chf2` | `"CHF "#,##0.00` | CHF 1,234.56 |
|
|
194
|
+
| `pct` | `0%` | 45% |
|
|
195
|
+
| `pct1` | `0.0%` | 45.1% |
|
|
196
|
+
| `pct2` | `0.00%` | 45.12% |
|
|
197
|
+
| `shortdate` | `d mmm yyyy` | 13 Jan 2025 |
|
|
198
|
+
| `longdate` | `d mmmm yyyy` | 13 January 2025 |
|
|
199
|
+
| `monthyear` | `mmm yyyy` | Jan 2025 |
|
|
200
|
+
|
|
201
|
+
Any string that isn't a preset name is treated as a raw Excel format code (ECMA-376). For example: `fmt="revenue:$#,##0.00"`.
|
|
202
|
+
|
|
203
|
+
Note: Percentage presets (`pct`, `pct1`, `pct2`) multiply by 100 per Excel convention — 0.45 displays as "45%".
|
|
204
|
+
|
|
205
|
+
### Usage Examples
|
|
206
|
+
|
|
207
|
+
```markdown
|
|
208
|
+
<!-- BigValue with currency -->
|
|
209
|
+
<BigValue data={total_revenue} value="total_revenue" title="Revenue" fmt="eur2" />
|
|
210
|
+
|
|
211
|
+
<!-- DataTable with per-column formatting -->
|
|
212
|
+
<DataTable data={sales} fmt="total_revenue:usd2,created_at:shortdate,margin:pct1" />
|
|
213
|
+
|
|
214
|
+
<!-- Chart with formatted tooltips -->
|
|
215
|
+
<BarChart data={monthly} x="month" y="revenue" yFmt="usd" />
|
|
216
|
+
<LineChart data={trend} x="date" y="growth" yFmt="pct1" />
|
|
217
|
+
```
|
|
218
|
+
|
|
129
219
|
## Field Names
|
|
130
220
|
|
|
131
221
|
Component field names (e.g. `x="city"`, `value="total_revenue"`) use the **unqualified** measure or dimension name — the same names defined in your cube. For example, if your cube has `measures: [{ name: total_revenue, ... }]`, use `value="total_revenue"`.
|
|
132
222
|
|
|
133
223
|
## See Also
|
|
134
224
|
|
|
135
|
-
- dashboards.queries
|
|
136
|
-
- dashboards.examples
|
|
137
|
-
- dashboards
|
|
225
|
+
- [Queries](dashboards.queries) — query syntax and properties
|
|
226
|
+
- [Examples](dashboards.examples) — complete dashboard examples
|
|
227
|
+
- [Dashboards](dashboards) — overview and deployment
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Examples
|
|
2
2
|
|
|
3
3
|
> Complete dashboard examples showing common patterns.
|
|
4
4
|
|
|
5
5
|
## Revenue Overview Dashboard
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The most common dashboard pattern: KPI cards at the top for at-a-glance metrics, a time series chart for trends, and a bar chart with data table for category breakdown.
|
|
8
8
|
|
|
9
9
|
```markdown
|
|
10
10
|
---
|
|
@@ -64,7 +64,7 @@ orderBy:
|
|
|
64
64
|
|
|
65
65
|
## Sales Pipeline Dashboard
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
A status-focused dashboard using a pie chart for proportional breakdown, a horizontal bar chart for ranking, and filters to drill into a specific segment.
|
|
68
68
|
|
|
69
69
|
```markdown
|
|
70
70
|
---
|
|
@@ -114,6 +114,161 @@ filters:
|
|
|
114
114
|
<AreaChart data={completed_trend} x="created_at" y="total_revenue" title="Completed Order Revenue" />
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
## Multi-Series Dashboard
|
|
118
|
+
|
|
119
|
+
When you need to compare segments side-by-side, use the `series` prop to split data by a dimension into colored segments. This example shows stacked bars, grouped bars, multi-line, and stacked area — all from the same data.
|
|
120
|
+
|
|
121
|
+
```markdown
|
|
122
|
+
---
|
|
123
|
+
title: Revenue by Channel
|
|
124
|
+
description: Multi-series charts showing revenue breakdown by sales channel
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
# Revenue by Channel
|
|
128
|
+
|
|
129
|
+
` ``query revenue_by_channel
|
|
130
|
+
cube: orders
|
|
131
|
+
measures: [total_revenue]
|
|
132
|
+
dimensions: [channel]
|
|
133
|
+
timeDimension:
|
|
134
|
+
dimension: created_at
|
|
135
|
+
granularity: month
|
|
136
|
+
dateRange: [2025-01-01, 2025-12-31]
|
|
137
|
+
` ``
|
|
138
|
+
|
|
139
|
+
## Stacked Bar (default)
|
|
140
|
+
|
|
141
|
+
<BarChart data={revenue_by_channel} x="created_at" y="total_revenue" series="channel" title="Revenue by Channel" />
|
|
142
|
+
|
|
143
|
+
## Grouped Bar
|
|
144
|
+
|
|
145
|
+
<BarChart data={revenue_by_channel} x="created_at" y="total_revenue" series="channel" type="grouped" title="Revenue by Channel (Grouped)" />
|
|
146
|
+
|
|
147
|
+
## Multi-Line
|
|
148
|
+
|
|
149
|
+
` ``query trend
|
|
150
|
+
cube: orders
|
|
151
|
+
measures: [total_revenue, count]
|
|
152
|
+
timeDimension:
|
|
153
|
+
dimension: created_at
|
|
154
|
+
granularity: month
|
|
155
|
+
dateRange: [2025-01-01, 2025-12-31]
|
|
156
|
+
` ``
|
|
157
|
+
|
|
158
|
+
<LineChart data={trend} x="created_at" y="total_revenue,count" title="Revenue vs Orders" />
|
|
159
|
+
|
|
160
|
+
## Stacked Area by Channel
|
|
161
|
+
|
|
162
|
+
<AreaChart data={revenue_by_channel} x="created_at" y="total_revenue" series="channel" type="stacked" title="Revenue by Channel" />
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Formatted Dashboard
|
|
166
|
+
|
|
167
|
+
Use format presets to display currencies, percentages, and number styles consistently across KPIs, charts, and tables.
|
|
168
|
+
|
|
169
|
+
```markdown
|
|
170
|
+
---
|
|
171
|
+
title: Sales Performance
|
|
172
|
+
description: Formatted revenue metrics and trends
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
# Sales Performance
|
|
176
|
+
|
|
177
|
+
` ``query totals
|
|
178
|
+
cube: orders
|
|
179
|
+
measures: [total_revenue, count, avg_order_value]
|
|
180
|
+
` ``
|
|
181
|
+
|
|
182
|
+
<Grid cols="3">
|
|
183
|
+
<BigValue data={totals} value="total_revenue" title="Revenue" fmt="eur2" />
|
|
184
|
+
<BigValue data={totals} value="count" title="Orders" fmt="num0" />
|
|
185
|
+
<BigValue data={totals} value="avg_order_value" title="Avg Order" fmt="eur2" />
|
|
186
|
+
</Grid>
|
|
187
|
+
|
|
188
|
+
## Revenue Trend
|
|
189
|
+
|
|
190
|
+
` ``query monthly
|
|
191
|
+
cube: orders
|
|
192
|
+
measures: [total_revenue]
|
|
193
|
+
timeDimension:
|
|
194
|
+
dimension: created_at
|
|
195
|
+
granularity: month
|
|
196
|
+
dateRange: [2025-01-01, 2025-12-31]
|
|
197
|
+
` ``
|
|
198
|
+
|
|
199
|
+
<LineChart data={monthly} x="created_at" y="total_revenue" title="Monthly Revenue" yFmt="eur" />
|
|
200
|
+
|
|
201
|
+
## Detail Table
|
|
202
|
+
|
|
203
|
+
` ``query details
|
|
204
|
+
cube: orders
|
|
205
|
+
measures: [total_revenue, count]
|
|
206
|
+
dimensions: [category]
|
|
207
|
+
orderBy:
|
|
208
|
+
total_revenue: desc
|
|
209
|
+
` ``
|
|
210
|
+
|
|
211
|
+
<DataTable data={details} fmt="total_revenue:eur2,count:num0" />
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Interactive Dashboard
|
|
215
|
+
|
|
216
|
+
Combine a DateRange picker and Dropdown filter to let viewers explore the data. Filter state syncs to the URL, so shared links preserve the exact filtered view.
|
|
217
|
+
|
|
218
|
+
```markdown
|
|
219
|
+
---
|
|
220
|
+
title: Interactive Sales
|
|
221
|
+
description: Sales dashboard with date and channel filters
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
# Interactive Sales
|
|
225
|
+
|
|
226
|
+
<DateRange name="period" default="last-6-months" label="Time Period" />
|
|
227
|
+
<Dropdown name="channel" dimension="channel" data={channels} queries="trend,by_city" label="Channel" />
|
|
228
|
+
|
|
229
|
+
` ``query channels
|
|
230
|
+
cube: orders
|
|
231
|
+
dimensions: [channel]
|
|
232
|
+
` ``
|
|
233
|
+
|
|
234
|
+
` ``query kpis
|
|
235
|
+
cube: orders
|
|
236
|
+
measures: [total_revenue, count]
|
|
237
|
+
` ``
|
|
238
|
+
|
|
239
|
+
<Grid cols="2">
|
|
240
|
+
<BigValue data={kpis} value="total_revenue" title="Revenue" fmt="eur2" />
|
|
241
|
+
<BigValue data={kpis} value="count" title="Orders" fmt="num0" />
|
|
242
|
+
</Grid>
|
|
243
|
+
|
|
244
|
+
## Revenue Trend
|
|
245
|
+
|
|
246
|
+
` ``query trend
|
|
247
|
+
cube: orders
|
|
248
|
+
measures: [total_revenue]
|
|
249
|
+
timeDimension:
|
|
250
|
+
dimension: created_at
|
|
251
|
+
granularity: month
|
|
252
|
+
` ``
|
|
253
|
+
|
|
254
|
+
<LineChart data={trend} x="created_at" y="total_revenue" title="Monthly Revenue" yFmt="eur" />
|
|
255
|
+
|
|
256
|
+
## By City
|
|
257
|
+
|
|
258
|
+
` ``query by_city
|
|
259
|
+
cube: orders
|
|
260
|
+
measures: [total_revenue]
|
|
261
|
+
dimensions: [city]
|
|
262
|
+
orderBy:
|
|
263
|
+
total_revenue: desc
|
|
264
|
+
limit: 10
|
|
265
|
+
` ``
|
|
266
|
+
|
|
267
|
+
<BarChart data={by_city} x="city" y="total_revenue" title="Top Cities" yFmt="eur" />
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The `<DateRange>` automatically applies to all queries with a `timeDimension` (here: `trend`). The `<Dropdown>` filters `trend` and `by_city` by channel. The `channels` query populates the dropdown and is never filtered by it.
|
|
271
|
+
|
|
117
272
|
## Tips
|
|
118
273
|
|
|
119
274
|
- **Start with KPIs**: Use `BigValue` in a `Grid` at the top for key metrics
|
|
@@ -122,9 +277,11 @@ filters:
|
|
|
122
277
|
- **Name queries descriptively**: `monthly_revenue` is better than `q1`
|
|
123
278
|
- **Limit large datasets**: Add `limit` to dimension queries to avoid oversized charts
|
|
124
279
|
- **Time series**: Always use `timeDimension` with `granularity` for time-based charts
|
|
280
|
+
- **Multi-series**: Use `series="column"` to split data by a dimension. For bars, default is stacked; use `type="grouped"` for side-by-side
|
|
281
|
+
- **Multiple y columns**: Use comma-separated values like `y="revenue,cases"` to show multiple measures on one chart
|
|
125
282
|
|
|
126
283
|
## See Also
|
|
127
284
|
|
|
128
|
-
- dashboards
|
|
129
|
-
- dashboards.queries
|
|
130
|
-
- dashboards.components
|
|
285
|
+
- [Dashboards](dashboards) — overview and deployment
|
|
286
|
+
- [Queries](dashboards.queries) — query syntax and properties
|
|
287
|
+
- [Components](dashboards.components) — chart and display components
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Inputs
|
|
2
|
+
|
|
3
|
+
> Interactive filter inputs for dashboards — date range pickers and dropdown selectors.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Inputs add interactivity to dashboards. They render as a filter bar above the charts and re-execute queries when values change. Inspired by Evidence.dev's inputs pattern, adapted for the Cube semantic layer.
|
|
8
|
+
|
|
9
|
+
Two input types are available:
|
|
10
|
+
|
|
11
|
+
- **DateRange** — preset date range picker that overrides `timeDimension.dateRange`
|
|
12
|
+
- **Dropdown** — dimension value selector that adds/replaces filters
|
|
13
|
+
|
|
14
|
+
## DateRange
|
|
15
|
+
|
|
16
|
+
Renders a date range preset picker. When changed, overrides `timeDimension.dateRange` on targeted queries.
|
|
17
|
+
|
|
18
|
+
```markdown
|
|
19
|
+
<DateRange name="period" default="last-6-months" label="Time Period" />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Props
|
|
23
|
+
|
|
24
|
+
| Prop | Type | Required | Description |
|
|
25
|
+
|------|------|----------|-------------|
|
|
26
|
+
| `name` | string | Yes | Unique input name |
|
|
27
|
+
| `default` | preset key | No | Initial preset (default: `last-6-months`) |
|
|
28
|
+
| `label` | string | No | Label shown above the picker |
|
|
29
|
+
| `queries` | string | No | Comma-separated query names to target |
|
|
30
|
+
|
|
31
|
+
### Targeting
|
|
32
|
+
|
|
33
|
+
Targeting lets you control which queries a filter affects — useful when some charts should stay fixed while others respond to filter changes.
|
|
34
|
+
|
|
35
|
+
- **No `queries` prop** — applies to ALL queries that have a `timeDimension`
|
|
36
|
+
- **With `queries` prop** — only applies to the listed queries
|
|
37
|
+
|
|
38
|
+
### Presets
|
|
39
|
+
|
|
40
|
+
| Key | Label |
|
|
41
|
+
|-----|-------|
|
|
42
|
+
| `last-7-days` | Last 7 Days |
|
|
43
|
+
| `last-30-days` | Last 30 Days |
|
|
44
|
+
| `last-3-months` | Last 3 Months |
|
|
45
|
+
| `last-6-months` | Last 6 Months |
|
|
46
|
+
| `last-12-months` | Last 12 Months |
|
|
47
|
+
| `month-to-date` | Month to Date |
|
|
48
|
+
| `year-to-date` | Year to Date |
|
|
49
|
+
| `last-year` | Last Year |
|
|
50
|
+
| `all-time` | All Time |
|
|
51
|
+
|
|
52
|
+
## Dropdown
|
|
53
|
+
|
|
54
|
+
Renders a dropdown selector populated from a query's dimension values. Adds a filter on the specified dimension to targeted queries.
|
|
55
|
+
|
|
56
|
+
```markdown
|
|
57
|
+
<Dropdown name="channel" dimension="channel" data={channels} queries="main,trend" label="Channel" />
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Props
|
|
61
|
+
|
|
62
|
+
| Prop | Type | Required | Description |
|
|
63
|
+
|------|------|----------|-------------|
|
|
64
|
+
| `name` | string | Yes | Unique input name |
|
|
65
|
+
| `dimension` | string | Yes | Dimension to filter on |
|
|
66
|
+
| `data` | query ref | Yes | Query that provides the dropdown options |
|
|
67
|
+
| `queries` | string | Yes | Comma-separated query names to filter |
|
|
68
|
+
| `label` | string | No | Label shown above the dropdown |
|
|
69
|
+
| `default` | string | No | Initial selected value (default: All) |
|
|
70
|
+
|
|
71
|
+
### Behavior
|
|
72
|
+
|
|
73
|
+
- Always includes an "All" option that removes the filter
|
|
74
|
+
- The dropdown's own `data` query is never filtered by itself (prevents circular dependencies)
|
|
75
|
+
- `queries` is required — the dropdown only filters explicitly listed queries
|
|
76
|
+
|
|
77
|
+
## Examples
|
|
78
|
+
|
|
79
|
+
### DateRange only
|
|
80
|
+
|
|
81
|
+
```markdown
|
|
82
|
+
---
|
|
83
|
+
title: Revenue Trends
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
<DateRange name="period" default="last-6-months" label="Time Period" />
|
|
87
|
+
|
|
88
|
+
` ``query monthly_revenue
|
|
89
|
+
cube: orders
|
|
90
|
+
measures: [total_revenue]
|
|
91
|
+
timeDimension:
|
|
92
|
+
dimension: created_at
|
|
93
|
+
granularity: month
|
|
94
|
+
` ``
|
|
95
|
+
|
|
96
|
+
<LineChart data={monthly_revenue} x="created_at" y="total_revenue" />
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The DateRange automatically applies to `monthly_revenue` because it has a `timeDimension`. No hardcoded `dateRange` needed in the query.
|
|
100
|
+
|
|
101
|
+
### Dropdown with query binding
|
|
102
|
+
|
|
103
|
+
```markdown
|
|
104
|
+
---
|
|
105
|
+
title: Sales by Channel
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
` ``query channels
|
|
109
|
+
cube: orders
|
|
110
|
+
dimensions: [channel]
|
|
111
|
+
` ``
|
|
112
|
+
|
|
113
|
+
<Dropdown name="ch" dimension="channel" data={channels} queries="main" label="Channel" />
|
|
114
|
+
|
|
115
|
+
` ``query main
|
|
116
|
+
cube: orders
|
|
117
|
+
measures: [total_revenue]
|
|
118
|
+
dimensions: [city]
|
|
119
|
+
` ``
|
|
120
|
+
|
|
121
|
+
<BarChart data={main} x="city" y="total_revenue" />
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Combined inputs
|
|
125
|
+
|
|
126
|
+
```markdown
|
|
127
|
+
---
|
|
128
|
+
title: Sales Dashboard
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
<DateRange name="period" default="last-6-months" label="Time Period" />
|
|
132
|
+
<Dropdown name="channel" dimension="channel" data={channels} queries="trend,by_city" label="Channel" />
|
|
133
|
+
|
|
134
|
+
` ``query channels
|
|
135
|
+
cube: orders
|
|
136
|
+
dimensions: [channel]
|
|
137
|
+
` ``
|
|
138
|
+
|
|
139
|
+
` ``query trend
|
|
140
|
+
cube: orders
|
|
141
|
+
measures: [total_revenue]
|
|
142
|
+
timeDimension:
|
|
143
|
+
dimension: created_at
|
|
144
|
+
granularity: month
|
|
145
|
+
` ``
|
|
146
|
+
|
|
147
|
+
<LineChart data={trend} x="created_at" y="total_revenue" />
|
|
148
|
+
|
|
149
|
+
` ``query by_city
|
|
150
|
+
cube: orders
|
|
151
|
+
measures: [total_revenue]
|
|
152
|
+
dimensions: [city]
|
|
153
|
+
` ``
|
|
154
|
+
|
|
155
|
+
<BarChart data={by_city} x="city" y="total_revenue" />
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Both inputs work together: the DateRange scopes the time window on `trend` (which has a timeDimension), and the Dropdown filters both `trend` and `by_city` by channel.
|
|
159
|
+
|
|
160
|
+
## Shareable URLs
|
|
161
|
+
|
|
162
|
+
Input values automatically sync to URL query params. When a user changes a filter, the URL updates to reflect the current state — for example:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
/dashboards/sales?period=last-30-days&channel=Online
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- **Default values are omitted** from the URL for clean links
|
|
169
|
+
- **Sharing a URL** preserves the current filter state — recipients see exactly the same filtered view
|
|
170
|
+
- **Refreshing the page** restores the active filters from the URL
|
|
171
|
+
- The **Copy Link** button in the dashboard header copies the full URL including filter params
|
|
172
|
+
|
|
173
|
+
DateRange inputs store the preset key (e.g. `last-30-days`), so shared URLs stay relative to today. Dropdown inputs store the selected value string.
|
|
174
|
+
|
|
175
|
+
## See Also
|
|
176
|
+
|
|
177
|
+
- [Dashboards](dashboards) — overview and deployment
|
|
178
|
+
- [Components](dashboards.components) — chart and display components
|
|
179
|
+
- [Examples](dashboards.examples) — complete dashboard examples
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dashboards
|
|
2
2
|
|
|
3
3
|
> Build interactive dashboards from markdown with embedded semantic layer queries.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
Dashboards
|
|
7
|
+
Dashboards let your team track key metrics without leaving the Bonnard app. Define queries once in markdown, deploy them, and every viewer gets live, governed data — no separate BI tool needed. Filters, formatting, and layout are all declared in the same file.
|
|
8
|
+
|
|
9
|
+
A dashboard is a markdown file with YAML frontmatter, query blocks, and chart components. Write it as a `.md` file, deploy with `bon dashboard deploy`, and view it in the Bonnard web app.
|
|
8
10
|
|
|
9
11
|
## Format
|
|
10
12
|
|
|
@@ -60,6 +62,8 @@ slug: revenue-dashboard # Optional (derived from title if omitted)
|
|
|
60
62
|
|
|
61
63
|
## Deployment
|
|
62
64
|
|
|
65
|
+
Deploy from the command line or via MCP tools. Each deploy auto-versions the dashboard so you can roll back if needed.
|
|
66
|
+
|
|
63
67
|
```bash
|
|
64
68
|
# Deploy a single dashboard
|
|
65
69
|
bon dashboard deploy revenue.md
|
|
@@ -76,8 +80,38 @@ bon dashboard remove revenue-dashboard
|
|
|
76
80
|
|
|
77
81
|
Via MCP tools, agents can use `deploy_dashboard` with the markdown content as a string.
|
|
78
82
|
|
|
83
|
+
## Versioning
|
|
84
|
+
|
|
85
|
+
Every deployment auto-increments the version number and saves a snapshot. You can view version history and restore previous versions:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Via MCP tools:
|
|
89
|
+
# get_dashboard with version parameter to fetch a specific version
|
|
90
|
+
# deploy_dashboard with slug + restore_version to roll back
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Restoring a version creates a new version (e.g. restoring v2 from v5 creates v6 with v2's content). Version history is never deleted — only `remove_dashboard` deletes all history.
|
|
94
|
+
|
|
95
|
+
## Sharing
|
|
96
|
+
|
|
97
|
+
Dashboard viewers include a **Share** menu in the header with:
|
|
98
|
+
|
|
99
|
+
- **Copy link** — copies the current URL including any active filter state
|
|
100
|
+
- **Print to PDF** — opens the browser print dialog for PDF export
|
|
101
|
+
|
|
102
|
+
Filter state (DateRange presets, Dropdown selections) is encoded in URL query params, so shared links preserve the exact filtered view the sender was looking at.
|
|
103
|
+
|
|
104
|
+
## Governance
|
|
105
|
+
|
|
106
|
+
Dashboard queries respect the same governance policies as all other queries. When a user views a dashboard:
|
|
107
|
+
|
|
108
|
+
- **View-level access** — users only see data from views their governance groups allow
|
|
109
|
+
- **Row-level filtering** — user attributes (e.g. region, department) automatically filter query results
|
|
110
|
+
- All org members see the same dashboard list, but may see different data depending on their governance context
|
|
111
|
+
|
|
79
112
|
## See Also
|
|
80
113
|
|
|
81
|
-
- dashboards.queries
|
|
82
|
-
- dashboards.components
|
|
83
|
-
- dashboards.
|
|
114
|
+
- [Queries](dashboards.queries) — query syntax and properties
|
|
115
|
+
- [Components](dashboards.components) — chart and display components
|
|
116
|
+
- [Inputs](dashboards.inputs) — interactive filters
|
|
117
|
+
- [Examples](dashboards.examples) — complete dashboard examples
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Queries
|
|
2
2
|
|
|
3
3
|
> Define data queries in dashboard markdown using YAML code fences.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Each query fetches data from your semantic layer and makes it available to chart components. Queries use the same measures and dimensions defined in your cubes and views — field names stay consistent whether you're querying from a dashboard, MCP, or the API.
|
|
8
|
+
|
|
9
|
+
Query blocks have a unique name and map to a `QueryOptions` shape. Components reference them using `data={query_name}`. Field names are unqualified — use `count` not `orders.count` — because the `cube` property provides the context.
|
|
8
10
|
|
|
9
11
|
## Syntax
|
|
10
12
|
|
|
@@ -112,6 +114,6 @@ filters:
|
|
|
112
114
|
|
|
113
115
|
## See Also
|
|
114
116
|
|
|
115
|
-
- dashboards.components
|
|
116
|
-
- dashboards
|
|
117
|
-
-
|
|
117
|
+
- [Components](dashboards.components) — chart and display components
|
|
118
|
+
- [Dashboards](dashboards) — overview and deployment
|
|
119
|
+
- [Querying](querying) — query format reference
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Views are facades that expose selected measures and dimensions from one or more cubes. They define which data is available to consumers, control join paths, and organize members into logical groups.
|
|
8
8
|
|
|
9
|
-
**Views should represent how a team thinks about data**, not mirror your warehouse tables. Name views by what they answer (`sales_pipeline`, `customer_insights`) rather than what table they wrap (`orders_view`, `users_view`).
|
|
9
|
+
**Views should represent how a team thinks about data**, not mirror your warehouse tables. Name views by what they answer (`sales_pipeline`, `customer_insights`) rather than what table they wrap (`orders_view`, `users_view`). Build as many views as your audiences need, but make each one purposeful — governance policies control which views each user or role can access.
|
|
10
10
|
|
|
11
11
|
## Example
|
|
12
12
|
|
|
@@ -30,10 +30,11 @@ If you have a BI tool (Metabase, Looker, Tableau), your top dashboards
|
|
|
30
30
|
by view count are the best source of real questions. If not, ask each team:
|
|
31
31
|
"What 3 numbers do you check every week?"
|
|
32
32
|
|
|
33
|
-
**Why this matters:** A semantic layer built from questions
|
|
34
|
-
views. One built from tables
|
|
35
|
-
between.
|
|
36
|
-
|
|
33
|
+
**Why this matters:** A semantic layer built from questions produces focused,
|
|
34
|
+
audience-scoped views. One built from tables produces generic views that agents
|
|
35
|
+
struggle to choose between. Governance policies control which views each user
|
|
36
|
+
or role can access, so build as many views as your audiences need — but make
|
|
37
|
+
each one purposeful with clear descriptions.
|
|
37
38
|
|
|
38
39
|
## Principle 2: Views Are for Audiences, Not Tables
|
|
39
40
|
|
|
@@ -29,10 +29,11 @@ If you have a BI tool (Metabase, Looker, Tableau), your top dashboards
|
|
|
29
29
|
by view count are the best source of real questions. If not, ask each team:
|
|
30
30
|
"What 3 numbers do you check every week?"
|
|
31
31
|
|
|
32
|
-
**Why this matters:** A semantic layer built from questions
|
|
33
|
-
views. One built from tables
|
|
34
|
-
between.
|
|
35
|
-
|
|
32
|
+
**Why this matters:** A semantic layer built from questions produces focused,
|
|
33
|
+
audience-scoped views. One built from tables produces generic views that agents
|
|
34
|
+
struggle to choose between. Governance policies control which views each user
|
|
35
|
+
or role can access, so build as many views as your audiences need — but make
|
|
36
|
+
each one purposeful with clear descriptions.
|
|
36
37
|
|
|
37
38
|
## Principle 2: Views Are for Audiences, Not Tables
|
|
38
39
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bonnard/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bon": "./dist/bin/bon.mjs"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"dist"
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "tsdown src/bin/bon.ts --format esm --out-dir dist/bin && cp -r src/templates dist/ && mkdir -p dist/docs/topics dist/docs/schemas && cp ../content/index.md dist/docs/_index.md && cp ../content/overview.md ../content/getting-started.md dist/docs/topics/ && cp ../content/modeling/*.md dist/docs/topics/",
|
|
12
|
+
"build": "tsdown src/bin/bon.ts --format esm --out-dir dist/bin && cp -r src/templates dist/ && mkdir -p dist/docs/topics dist/docs/schemas && cp ../content/index.md dist/docs/_index.md && cp ../content/overview.md ../content/getting-started.md dist/docs/topics/ && cp ../content/modeling/*.md dist/docs/topics/ && cp ../content/dashboards/*.md dist/docs/topics/",
|
|
13
13
|
"dev": "tsdown src/bin/bon.ts --format esm --out-dir dist/bin --watch",
|
|
14
14
|
"test": "vitest run"
|
|
15
15
|
},
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"tsdown": "^0.20.1",
|
|
28
28
|
"vitest": "^2.0.0"
|
|
29
29
|
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/meal-inc/bonnard-cli.git"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
30
35
|
"engines": {
|
|
31
36
|
"node": ">=20.0.0"
|
|
32
37
|
}
|