From 6319e0e2aa9eb2f7466e98fd6930707e1e316732 Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 11:33:14 -0600 Subject: [PATCH 01/13] docs: add the Bakehouse dashboard journey --- .../02-databricks-projects/dashboards.md | 952 ++++++++++++++++++ .../content/02-databricks-projects/index.md | 2 + .../02-databricks-projects/metric-views.md | 1 + docs/agentic-starter-journey/lib/nav.ts | 1 + 4 files changed, 956 insertions(+) create mode 100644 docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md new file mode 100644 index 0000000..60d911e --- /dev/null +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -0,0 +1,952 @@ +--- +description: Create, publish, and verify the Bakehouse Franchise Performance dashboard from the governed metric view. +--- + +# Dashboards + +## Mental Model + +An AI/BI dashboard is a native DABs resource backed by a serialized `.lvdash.json` definition. +This page consumes the successful [Metric Views](/docs/02-databricks-projects/metric-views/) outcome. +Dataset SQL remains portable by using bare `franchise_sales_metrics` and putting the catalog and schema on the native resource. + +## Goal + +Create and publish `Bakehouse Franchise Performance`. +Produce three KPI counters, one daily sales line chart, and two horizontal bar charts. +Prove every local and deployed dataset with executable structure, row, order, and value assertions. + +## Prerequisites + +- Auth surface: `workspace`. +- Complete the [Metric Views](/docs/02-databricks-projects/metric-views/) outcome. +- Use an existing bundle with a `dev` target, `${var.catalog}`, `${var.warehouse_id}`, `resources/*.yml`, and the live `bakehouse_gold.franchise_sales_metrics` metric view. +- Confirm the principal can use the warehouse, query the metric view, and create and publish dashboards. +- Install Databricks CLI v1.1.0 or newer. + +## Skill + +Read these verified upstream skills in this order after the auth precheck passes: + +1. [`databricks-core`](https://github.com/databricks/databricks-agent-skills/tree/main/plugins/databricks/claude/skills/databricks-core). +2. [`databricks-aibi-dashboards`](https://github.com/databricks/databricks-agent-skills/tree/main/plugins/databricks/claude/skills/databricks-aibi-dashboards). +3. [`databricks-dabs`](https://github.com/databricks/databricks-agent-skills/tree/main/plugins/databricks/claude/skills/databricks-dabs). + +Test every SQL statement before deployment. + +## Inputs + +| Input | Source | How to obtain | +|---|---|---| +| Databricks account ID | Human-provided | Use the account ID named for this deployment | +| Workspace ID | Human-provided | Use the workspace ID named for this deployment | +| Workspace host | Human-provided | Use the named workspace URL | +| Workspace CLI profile | Human-provided | Use the named profile for the target workspace | +| Existing project path | Human-provided | Use the existing bundle project completed on the Metric Views page | +| Target catalog | Agent-derived | Read the active bundle target's `catalog` variable | +| SQL warehouse ID | Agent-derived | Read the active bundle target's `warehouse_id` variable | + +## Run + +### 0. Verify auth and active-target inputs + +Require every human-provided input and fail closed if the profile reaches another target: + +```bash +: "${DATABRICKS_ACCOUNT_ID:?set the human-provided Databricks account ID}" +: "${DATABRICKS_WORKSPACE_ID:?set the human-provided workspace ID}" +: "${DATABRICKS_HOST:?set the human-provided workspace host}" +: "${DATABRICKS_CONFIG_PROFILE:?set the human-provided workspace CLI profile}" +: "${BAKEHOUSE_PROJECT_PATH:?set the human-provided existing bundle project path}" + +cd "$BAKEHOUSE_PROJECT_PATH" + +auth_target=$( + databricks auth describe --profile "$DATABRICKS_CONFIG_PROFILE" -o json \ + | jq -ce \ + --arg account_id "$DATABRICKS_ACCOUNT_ID" \ + --arg workspace_id "$DATABRICKS_WORKSPACE_ID" \ + --arg host "$DATABRICKS_HOST" ' + { + host: (.host // .details.host // .details.configuration.host.value), + account_id: (.account_id // .details.configuration.account_id.value), + workspace_id: (.workspace_id // .details.configuration.workspace_id.value) + } + | select( + .host == $host + and .account_id == $account_id + and (.workspace_id | tostring) == $workspace_id + )' +) || { + printf '%s\n' 'blocked: auth preflight failed' >&2 + exit 1 +} + +printf '%s\n' "$auth_target" + +databricks current-user me --profile "$DATABRICKS_CONFIG_PROFILE" -o json \ + | jq -e 'select(.active == true and .id != null and .userName != null)' + +bundle_json=$( + databricks bundle validate \ + --strict \ + --target dev \ + --profile "$DATABRICKS_CONFIG_PROFILE" \ + -o json +) + +catalog=$(jq -er '.variables.catalog.value' <<<"$bundle_json") +warehouse_id=$(jq -er '.variables.warehouse_id.value' <<<"$bundle_json") +test -n "$catalog" +test -n "$warehouse_id" +dataset_schema=bakehouse_gold +``` + +Expected: auth matches every human-provided target, strict validation succeeds, and catalog and warehouse are read from the active bundle target. +Do not select another profile, catalog, warehouse, or project path after a mismatch. + +### 1. Define dataset execution and assertions + +Use one stable Statement Execution API helper for both source and deployed datasets: + +```bash +run_sql() { + local statement=$1 response statement_id state + + response=$( + databricks api post /api/2.0/sql/statements \ + --profile "$DATABRICKS_CONFIG_PROFILE" \ + --json "$(jq -n \ + --arg warehouse_id "$warehouse_id" \ + --arg catalog "$catalog" \ + --arg schema "$dataset_schema" \ + --arg statement "$statement" \ + '{ + warehouse_id: $warehouse_id, + catalog: $catalog, + schema: $schema, + statement: $statement, + wait_timeout: "0s" + }')" + ) || return + + statement_id=$(jq -er '.statement_id' <<<"$response") || return + + while :; do + state=$(jq -er '.status.state' <<<"$response") || return + case "$state" in + SUCCEEDED) + jq -e '.status.state == "SUCCEEDED" and .result.data_array != null' <<<"$response" >/dev/null + printf '%s\n' "$response" + return + ;; + PENDING|RUNNING) + sleep 5 + response=$( + databricks api get "/api/2.0/sql/statements/$statement_id" \ + --profile "$DATABRICKS_CONFIG_PROFILE" + ) || return + ;; + *) + jq -c '.status.error // .status' <<<"$response" >&2 + return 1 + ;; + esac + done +} + +dataset_sql() { + local dashboard_file=$1 dataset_name=$2 + jq -er \ + --arg dataset_name "$dataset_name" ' + [.datasets[] | select(.name == $dataset_name) | .queryLines[]] + | select(length > 0) + | join("")' \ + "$dashboard_file" +} + +assert_kpis() { + jq -e ' + [.manifest.schema.columns[].name] == [ + "total_sales", + "order_count", + "avg_order_value" + ] + and (.manifest.total_row_count == 1) + and (.result.data_array | length == 1) + and ( + .result.data_array[0] | map(tonumber) + | .[0] == 66471 + and .[1] == 3333 + and .[2] > 0 + and ((.[0] - (.[1] * .[2])) > -0.01) + and ((.[0] - (.[1] * .[2])) < 0.01) + )' +} + +assert_sales_trend() { + jq -e ' + [.manifest.schema.columns[].name] == ["sales_date", "total_sales"] + and (.manifest.total_row_count == 17) + and (.result.data_array | length == 17) + and ( + .result.data_array as $rows + | ([$rows[][0]] == ([$rows[][0]] | sort)) + and ($rows[0] == ["2024-05-01", "4128"]) + and ($rows[-1] == ["2024-05-17", "1932"]) + and ([$rows[][1] | tonumber] | add == 66471) + and all($rows[]; (.[0] | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}$")) and (.[1] | tonumber) > 0) + )' +} + +assert_franchises() { + jq -e ' + [.manifest.schema.columns[].name] == ["franchise", "total_sales"] + and (.manifest.total_row_count == 10) + and (.result.data_array | length == 10) + and (.result.data_array[0] == ["Baked Bliss", "6642"]) + and ( + .result.data_array as $rows + | ([$rows[][1] | tonumber] == ([$rows[][1] | tonumber] | sort | reverse)) + and all($rows[]; (.[0] | type == "string" and length > 0) and (.[1] | tonumber) > 0) + )' +} + +assert_products() { + jq -e ' + [.manifest.schema.columns[].name] == ["product", "total_sales"] + and (.manifest.total_row_count == 6) + and (.result.data_array | length == 6) + and (.result.data_array[0] == ["Golden Gate Ginger", "11595"]) + and ( + .result.data_array as $rows + | ([$rows[][1] | tonumber] == ([$rows[][1] | tonumber] | sort | reverse)) + and all($rows[]; (.[0] | type == "string" and length > 0) and (.[1] | tonumber) > 0) + )' +} + +assert_all_datasets() { + local dashboard_file=$1 sql + + sql=$(dataset_sql "$dashboard_file" ds_kpis) + run_sql "$sql" | assert_kpis + + sql=$(dataset_sql "$dashboard_file" ds_sales_trend) + run_sql "$sql" | assert_sales_trend + + sql=$(dataset_sql "$dashboard_file" ds_franchises) + run_sql "$sql" | assert_franchises + + sql=$(dataset_sql "$dashboard_file" ds_products) + run_sql "$sql" | assert_products +} +``` + +Every request uses the same active-target warehouse, catalog, and `bakehouse_gold` schema. +If the source data changes, stop and obtain human approval before changing the locked values. + +### 2. Create the dashboard source + +Create `src/bakehouse_franchise_performance.lvdash.json` with this complete definition: + +```json +{ + "datasets": [ + { + "name": "ds_kpis", + "displayName": "Franchise performance KPIs", + "queryLines": [ + "SELECT MEASURE(total_sales) AS total_sales,\n", + " MEASURE(order_count) AS order_count,\n", + " MEASURE(avg_order_value) AS avg_order_value\n", + "FROM franchise_sales_metrics " + ] + }, + { + "name": "ds_sales_trend", + "displayName": "Daily sales trend", + "queryLines": [ + "SELECT sales_date, MEASURE(total_sales) AS total_sales\n", + "FROM franchise_sales_metrics\n", + "GROUP BY sales_date\n", + "ORDER BY sales_date " + ] + }, + { + "name": "ds_franchises", + "displayName": "Top franchises", + "queryLines": [ + "SELECT franchise, MEASURE(total_sales) AS total_sales\n", + "FROM franchise_sales_metrics\n", + "GROUP BY franchise\n", + "ORDER BY total_sales DESC\n", + "LIMIT 10 " + ] + }, + { + "name": "ds_products", + "displayName": "Top products", + "queryLines": [ + "SELECT product, MEASURE(total_sales) AS total_sales\n", + "FROM franchise_sales_metrics\n", + "GROUP BY product\n", + "ORDER BY total_sales DESC\n", + "LIMIT 10 " + ] + } + ], + "pages": [ + { + "name": "overview", + "displayName": "Overview", + "pageType": "PAGE_TYPE_CANVAS", + "layoutVersion": "GRID_V1", + "layout": [ + { + "widget": { + "name": "dashboard-title", + "multilineTextboxSpec": { + "lines": [ + "# Bakehouse Franchise Performance" + ] + } + }, + "position": { + "x": 0, + "y": 0, + "width": 12, + "height": 1 + } + }, + { + "widget": { + "name": "dashboard-subtitle", + "multilineTextboxSpec": { + "lines": [ + "Daily sales, order volume, and product performance across the Bakehouse franchise network." + ] + } + }, + "position": { + "x": 0, + "y": 1, + "width": 12, + "height": 1 + } + }, + { + "widget": { + "name": "total-sales-kpi", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "ds_kpis", + "fields": [ + { + "name": "total_sales", + "expression": "`total_sales`" + } + ], + "disaggregated": true + } + } + ], + "spec": { + "version": 2, + "widgetType": "counter", + "encodings": { + "value": { + "fieldName": "total_sales", + "displayName": "Total Sales", + "format": { + "type": "number-plain", + "abbreviation": "compact", + "decimalPlaces": { + "type": "max", + "places": 2 + } + } + } + }, + "frame": { + "showTitle": true, + "title": "Total Sales" + } + } + }, + "position": { + "x": 0, + "y": 2, + "width": 4, + "height": 3 + } + }, + { + "widget": { + "name": "order-count-kpi", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "ds_kpis", + "fields": [ + { + "name": "order_count", + "expression": "`order_count`" + } + ], + "disaggregated": true + } + } + ], + "spec": { + "version": 2, + "widgetType": "counter", + "encodings": { + "value": { + "fieldName": "order_count", + "displayName": "Order Count", + "format": { + "type": "number-plain", + "decimalPlaces": { + "type": "exact", + "places": 0 + } + } + } + }, + "frame": { + "showTitle": true, + "title": "Order Count" + } + } + }, + "position": { + "x": 4, + "y": 2, + "width": 4, + "height": 3 + } + }, + { + "widget": { + "name": "average-order-value-kpi", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "ds_kpis", + "fields": [ + { + "name": "avg_order_value", + "expression": "`avg_order_value`" + } + ], + "disaggregated": true + } + } + ], + "spec": { + "version": 2, + "widgetType": "counter", + "encodings": { + "value": { + "fieldName": "avg_order_value", + "displayName": "Average Order Value", + "format": { + "type": "number-plain", + "decimalPlaces": { + "type": "exact", + "places": 2 + } + } + } + }, + "frame": { + "showTitle": true, + "title": "Average Order Value" + } + } + }, + "position": { + "x": 8, + "y": 2, + "width": 4, + "height": 3 + } + }, + { + "widget": { + "name": "daily-sales-trend", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "ds_sales_trend", + "fields": [ + { + "name": "sales_date", + "expression": "`sales_date`" + }, + { + "name": "total_sales", + "expression": "`total_sales`" + } + ], + "disaggregated": true + } + } + ], + "spec": { + "version": 3, + "widgetType": "line", + "encodings": { + "x": { + "fieldName": "sales_date", + "displayName": "Sales Date", + "scale": { + "type": "temporal" + } + }, + "y": { + "fieldName": "total_sales", + "displayName": "Total Sales", + "scale": { + "type": "quantitative", + "domainMin": 0 + }, + "format": { + "type": "number", + "abbreviation": "compact", + "decimalPlaces": { + "type": "max", + "places": 2 + } + } + } + }, + "frame": { + "showTitle": true, + "title": "Daily Sales Trend", + "showDescription": true, + "description": "Total sales by calendar day." + } + } + }, + "position": { + "x": 0, + "y": 5, + "width": 12, + "height": 6 + } + }, + { + "widget": { + "name": "top-franchises", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "ds_franchises", + "fields": [ + { + "name": "franchise", + "expression": "`franchise`" + }, + { + "name": "total_sales", + "expression": "`total_sales`" + } + ], + "disaggregated": true + } + } + ], + "spec": { + "version": 3, + "widgetType": "bar", + "encodings": { + "x": { + "fieldName": "total_sales", + "displayName": "Total Sales", + "scale": { + "type": "quantitative", + "domainMin": 0 + }, + "format": { + "type": "number", + "abbreviation": "compact", + "decimalPlaces": { + "type": "max", + "places": 2 + } + } + }, + "y": { + "fieldName": "franchise", + "displayName": "Franchise", + "scale": { + "type": "categorical" + } + } + }, + "frame": { + "showTitle": true, + "title": "Top Franchises", + "showDescription": true, + "description": "Ten franchises with the highest total sales." + } + } + }, + "position": { + "x": 0, + "y": 11, + "width": 6, + "height": 6 + } + }, + { + "widget": { + "name": "top-products", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "ds_products", + "fields": [ + { + "name": "product", + "expression": "`product`" + }, + { + "name": "total_sales", + "expression": "`total_sales`" + } + ], + "disaggregated": true + } + } + ], + "spec": { + "version": 3, + "widgetType": "bar", + "encodings": { + "x": { + "fieldName": "total_sales", + "displayName": "Total Sales", + "scale": { + "type": "quantitative", + "domainMin": 0 + }, + "format": { + "type": "number", + "abbreviation": "compact", + "decimalPlaces": { + "type": "max", + "places": 2 + } + } + }, + "y": { + "fieldName": "product", + "displayName": "Product", + "scale": { + "type": "categorical" + } + } + }, + "frame": { + "showTitle": true, + "title": "Top Products", + "showDescription": true, + "description": "Products ranked by total sales." + } + } + }, + "position": { + "x": 6, + "y": 11, + "width": 6, + "height": 6 + } + } + ] + } + ], + "uiSettings": { + "theme": { + "canvasBackgroundColor": { + "light": "#F5F7FA", + "dark": "#111827" + }, + "widgetBackgroundColor": { + "light": "#FFFFFF", + "dark": "#1F2937" + }, + "widgetBorderColor": { + "light": "#FFFFFF", + "dark": "#1F2937" + }, + "fontColor": { + "light": "#172033", + "dark": "#F3F4F6" + }, + "selectionColor": { + "light": "#0072B2", + "dark": "#56B4E9" + }, + "visualizationColors": [ + "#0072B2", + "#E69F00", + "#009E73", + "#CC79A7", + "#D55E00", + "#56B4E9" + ], + "widgetHeaderAlignment": "LEFT", + "fontFamily": "Inter", + "widgetCornerRadius": 8 + } + } +} +``` + +Parse the source and execute all four exact queries before deployment: + +```bash +dashboard_file=src/bakehouse_franchise_performance.lvdash.json +jq -e '.' "$dashboard_file" >/dev/null +assert_all_datasets "$dashboard_file" +``` + +Expected: JSON parsing and all source dataset assertions succeed. + +### 3. Create the native dashboard resource + +Create `resources/bakehouse_franchise_performance.dashboard.yml`: + +```yaml +resources: + dashboards: + bakehouse_franchise_performance: + display_name: Bakehouse Franchise Performance + file_path: ../src/bakehouse_franchise_performance.lvdash.json + warehouse_id: ${var.warehouse_id} + dataset_catalog: ${var.catalog} + dataset_schema: bakehouse_gold +``` + +Strictly validate the local configuration: + +```bash +databricks bundle validate \ + --strict \ + --target dev \ + --profile "$DATABRICKS_CONFIG_PROFILE" +``` + +Expected: strict validation succeeds with the dashboard resource key and all native fields. + +### 4. Deploy and publish + +Deploy, resolve the dashboard ID only through bundle summary, and publish with positional CLI syntax: + +```bash +databricks bundle deploy \ + --target dev \ + --profile "$DATABRICKS_CONFIG_PROFILE" + +summary=$( + databricks bundle summary \ + --target dev \ + --profile "$DATABRICKS_CONFIG_PROFILE" \ + -o json +) + +dashboard_id=$( + jq -er ' + .resources.dashboards.bakehouse_franchise_performance.id + | tostring + | select(length > 0)' \ + <<<"$summary" +) + +dashboard_url=$( + jq -er '.resources.dashboards.bakehouse_franchise_performance.url' \ + <<<"$summary" +) + +printf 'dashboard_id=%s\ndashboard_url=%s\n' "$dashboard_id" "$dashboard_url" + +published_after_publish=$( + databricks lakeview publish "$dashboard_id" \ + --warehouse-id "$warehouse_id" \ + --profile "$DATABRICKS_CONFIG_PROFILE" \ + -o json +) + +jq -e \ + --arg warehouse_id "$warehouse_id" ' + .display_name == "Bakehouse Franchise Performance" + and .warehouse_id == $warehouse_id + and (.revision_create_time | type == "string" and length > 0)' \ + <<<"$published_after_publish" +``` + +Expected: deployment succeeds, summary returns one nonempty dashboard ID and URL, and publish returns the exact display name, warehouse, and a revision timestamp. + +## Verify + +Retrieve and assert the draft and published objects with positional IDs: + +```bash +draft=$( + databricks lakeview get "$dashboard_id" \ + --profile "$DATABRICKS_CONFIG_PROFILE" \ + -o json +) + +published=$( + databricks lakeview get-published "$dashboard_id" \ + --profile "$DATABRICKS_CONFIG_PROFILE" \ + -o json +) + +jq -e \ + --arg dashboard_id "$dashboard_id" \ + --arg warehouse_id "$warehouse_id" \ + --arg catalog "$catalog" ' + .dashboard_id == $dashboard_id + and .display_name == "Bakehouse Franchise Performance" + and .warehouse_id == $warehouse_id + and .dataset_catalog == $catalog + and .dataset_schema == "bakehouse_gold" + and (.serialized_dashboard | type == "string" and length > 0)' \ + <<<"$draft" + +jq -e \ + --arg warehouse_id "$warehouse_id" ' + .display_name == "Bakehouse Franchise Performance" + and .warehouse_id == $warehouse_id + and (.revision_create_time | type == "string" and length > 0)' \ + <<<"$published" +``` + +Expected: draft metadata and serialized content match the requested resource, and the published object has the exact name, warehouse, and revision timestamp. + +Extract and assert the deployed serialized dashboard: + +```bash +deployed_dashboard=$(mktemp) +trap 'rm -f "$deployed_dashboard"' EXIT + +jq -er '.serialized_dashboard | fromjson' <<<"$draft" >"$deployed_dashboard" + +jq -e ' + ([.datasets[].name] == [ + "ds_kpis", + "ds_sales_trend", + "ds_franchises", + "ds_products" + ]) + and (.datasets | length == 4) + and (.pages | length == 1) + and (.pages[0].name == "overview") + and (.pages[0].pageType == "PAGE_TYPE_CANVAS") + and (.pages[0].layoutVersion == "GRID_V1") + and (.pages[0].layout | length == 8) + and ( + [ + .pages[0].layout[].widget + | select(.spec != null) + | { + title: .spec.frame.title, + type: .spec.widgetType, + version: .spec.version + } + ] == [ + {"title":"Total Sales","type":"counter","version":2}, + {"title":"Order Count","type":"counter","version":2}, + {"title":"Average Order Value","type":"counter","version":2}, + {"title":"Daily Sales Trend","type":"line","version":3}, + {"title":"Top Franchises","type":"bar","version":3}, + {"title":"Top Products","type":"bar","version":3} + ] + ) + and ( + [ + .pages[0].layout[].widget + | select(.spec != null) + | { + name, + dataset: .queries[0].query.datasetName, + fields: [.queries[0].query.fields[].name], + encodings: ( + .spec.encodings + | [ + .value.fieldName?, + .x.fieldName?, + .y.fieldName? + ] + | map(select(. != null)) + ) + } + ] == [ + {"name":"total-sales-kpi","dataset":"ds_kpis","fields":["total_sales"],"encodings":["total_sales"]}, + {"name":"order-count-kpi","dataset":"ds_kpis","fields":["order_count"],"encodings":["order_count"]}, + {"name":"average-order-value-kpi","dataset":"ds_kpis","fields":["avg_order_value"],"encodings":["avg_order_value"]}, + {"name":"daily-sales-trend","dataset":"ds_sales_trend","fields":["sales_date","total_sales"],"encodings":["sales_date","total_sales"]}, + {"name":"top-franchises","dataset":"ds_franchises","fields":["franchise","total_sales"],"encodings":["total_sales","franchise"]}, + {"name":"top-products","dataset":"ds_products","fields":["product","total_sales"],"encodings":["total_sales","product"]} + ] + ) + and ([.datasets[].queryLines | join("") | contains("FROM franchise_sales_metrics")] | all) + and ([.datasets[].queryLines | join("") | test("FROM[[:space:]]+[^[:space:]]*\\.[^[:space:]]*franchise_sales_metrics")] | any | not) + and ([.pages[].layout[].widget.spec.widgetType? | select(. != null) | startswith("filter")] | any | not) + and (.uiSettings.genieSpace? == null) + and (.uiSettings.theme.canvasBackgroundColor.light == "#F5F7FA") + and (.uiSettings.theme.canvasBackgroundColor.dark == "#111827") + and (.uiSettings.theme.widgetBackgroundColor.light == "#FFFFFF") + and (.uiSettings.theme.widgetBackgroundColor.dark == "#1F2937") + and (.uiSettings.theme.fontColor.light == "#172033") + and (.uiSettings.theme.fontColor.dark == "#F3F4F6") +' "$deployed_dashboard" +``` + +Expected: the deployed object has exactly four datasets, eight layout widgets, six exact business widgets, exact versions and bindings, bare metric-view references, no filters, no Genie link, and the complete light and dark theme. + +Execute the deployed queries through the same API assertions: + +```bash +assert_all_datasets "$deployed_dashboard" +``` + +Expected: all four deployed datasets satisfy the same row and value assertions as the source. +No visual inspection may substitute for these executable checks. + +## Where this fails + +| Symptom | Cause | Fix | +|---|---|---| +| Auth precheck fails or target values mismatch | A required input is missing or the profile reaches another target | Reauthenticate the named profile against the named host and repeat the full precheck | +| `catalog` or `warehouse_id` extraction fails | The active `dev` target does not define both variables | Add the missing target values, strictly validate, and rerun extraction | +| Dataset SQL reports a missing metric view or unsupported function | The governed metric view is absent or the warehouse is incompatible | Complete Metric Views and use a compatible warehouse before continuing | +| A dashboard query resolves the wrong namespace | A `queryLines` statement qualifies `franchise_sales_metrics` | Restore the bare table name and keep namespace values on the resource | +| Statement execution resolves the wrong namespace | The API payload omits `catalog` or `schema` | Pass the active-target catalog and `bakehouse_gold` in every request | +| A local dataset assertion fails | Source data, SQL, columns, rows, ordering, or values drifted | Stop and reconcile the source contract before deployment | +| SQL contains merged tokens or swallowed lines | A `queryLines` fragment lacks a trailing space or newline | Restore the exact locked fragments and rerun all source assertions | +| A widget is invalid | A counter is not version 2 or a line or bar is not version 3 | Restore the exact widget versions | +| A widget has no selected fields | A query field name differs from its encoding field name | Restore the exact locked field bindings | +| Strict bundle validation fails | The native resource is malformed or its source path is wrong | Restore the exact resource and the `../src` path | +| Dashboard ID extraction fails | Bundle summary lacks the exact resource key | Inspect deployment output and restore `bakehouse_franchise_performance` | +| Publish fails or the published check fails | The principal cannot publish, the warehouse is wrong, or no published revision exists | Correct permission or warehouse access, republish the positional ID, and repeat both checks | +| Deployed structure assertion fails | Server serialization or the deployed source differs from the locked contract | Compare the extracted object with the source, correct the source, and redeploy | +| Deployed dataset assertion fails | Deployed SQL or source values differ from the verified local contract | Stop and reconcile the extracted queries and metric-view data | +| Bundle deployment reports drift after UI edits | The workspace draft was changed outside the bundle | Treat the committed source as authoritative and redeploy it through the bundle | + +## Next + +- **Back to section:** [Databricks Projects](/docs/02-databricks-projects/) +- **Reference:** [AI/BI dashboards](https://docs.databricks.com/aws/en/dashboards/) +- **Reference:** [Dashboard catalog and schema parameterization](https://docs.databricks.com/aws/en/dev-tools/bundles/examples#dashboard-catalog-and-schema-parameterization) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/index.md b/docs/agentic-starter-journey/content/02-databricks-projects/index.md index 62067ce..3f23fed 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/index.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/index.md @@ -16,6 +16,7 @@ One repo, one bundle, one owning team. The project repo and bundle must exist before any pipeline lands in it. The third page reads the shared Bakehouse batch source and publishes governed bronze and silver materialized views. The fourth page adds governed measures over those silver views and verifies them against raw SQL. +The fifth page deploys and publishes the Bakehouse franchise-performance dashboard from the governed metric view. | Order | Page | Skill | Status | |---|---|---|---| @@ -23,6 +24,7 @@ The fourth page adds governed measures over those silver views and verifies them | 2 | [Ingestion Pipelines](/docs/02-databricks-projects/ingestion-pipelines/) | `databricks-lakeflow-connect`, `databricks-zerobus-ingest` | Done | | 3 | [Spark Declarative Pipelines](/docs/02-databricks-projects/etl-pipelines/) | `databricks-pipelines` | Done | | 4 | [Metric Views](/docs/02-databricks-projects/metric-views/) | `databricks-metric-views` | Done | +| 5 | [Dashboards](/docs/02-databricks-projects/dashboards/) | `databricks-core`, `databricks-aibi-dashboards`, `databricks-dabs` | Done | Pages are linked as they are added. diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/metric-views.md b/docs/agentic-starter-journey/content/02-databricks-projects/metric-views.md index 3fcd599..ee67c04 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/metric-views.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/metric-views.md @@ -547,5 +547,6 @@ Expected: the aggregate query returns exactly one row, `metric_rows` is greater ## Next +- **Do next:** [Dashboards](/docs/02-databricks-projects/dashboards/) - **Back to section:** [Databricks Projects](/docs/02-databricks-projects/) - **Reference:** [Unity Catalog metric views](https://docs.databricks.com/metric-views/) diff --git a/docs/agentic-starter-journey/lib/nav.ts b/docs/agentic-starter-journey/lib/nav.ts index 7affb5a..76656c9 100644 --- a/docs/agentic-starter-journey/lib/nav.ts +++ b/docs/agentic-starter-journey/lib/nav.ts @@ -40,6 +40,7 @@ export const SECTIONS: Section[] = [ { label: "Ingestion Pipelines", slug: "02-databricks-projects/ingestion-pipelines" }, { label: "Spark Declarative Pipelines", slug: "02-databricks-projects/etl-pipelines" }, { label: "Metric Views", slug: "02-databricks-projects/metric-views" }, + { label: "Dashboards", slug: "02-databricks-projects/dashboards" }, ], }, ]; From ff7a588618bac7ac5c6e455c1e5300a1068a5a57 Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 11:45:44 -0600 Subject: [PATCH 02/13] fix: make dashboard verification fail closed Validate namespace settings at the bundle boundary and stop on any dataset, result, or theme drift. --- .../02-databricks-projects/dashboards.md | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index 60d911e..1ed412c 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -136,7 +136,7 @@ run_sql() { state=$(jq -er '.status.state' <<<"$response") || return case "$state" in SUCCEEDED) - jq -e '.status.state == "SUCCEEDED" and .result.data_array != null' <<<"$response" >/dev/null + jq -e '.status.state == "SUCCEEDED" and .result.data_array != null' <<<"$response" >/dev/null || return printf '%s\n' "$response" return ;; @@ -227,18 +227,19 @@ assert_products() { assert_all_datasets() { local dashboard_file=$1 sql + set -o pipefail - sql=$(dataset_sql "$dashboard_file" ds_kpis) - run_sql "$sql" | assert_kpis + sql=$(dataset_sql "$dashboard_file" ds_kpis) || return + run_sql "$sql" | assert_kpis || return - sql=$(dataset_sql "$dashboard_file" ds_sales_trend) - run_sql "$sql" | assert_sales_trend + sql=$(dataset_sql "$dashboard_file" ds_sales_trend) || return + run_sql "$sql" | assert_sales_trend || return - sql=$(dataset_sql "$dashboard_file" ds_franchises) - run_sql "$sql" | assert_franchises + sql=$(dataset_sql "$dashboard_file" ds_franchises) || return + run_sql "$sql" | assert_franchises || return - sql=$(dataset_sql "$dashboard_file" ds_products) - run_sql "$sql" | assert_products + sql=$(dataset_sql "$dashboard_file" ds_products) || return + run_sql "$sql" | assert_products || return } ``` @@ -764,6 +765,16 @@ summary=$( -o json ) +jq -e \ + --arg catalog "$catalog" ' + .resources.dashboards.bakehouse_franchise_performance + | .id != null + and (.id | tostring | length > 0) + and (.url | type == "string" and length > 0) + and .dataset_catalog == $catalog + and .dataset_schema == "bakehouse_gold"' \ + <<<"$summary" + dashboard_id=$( jq -er ' .resources.dashboards.bakehouse_franchise_performance.id @@ -815,13 +826,10 @@ published=$( jq -e \ --arg dashboard_id "$dashboard_id" \ - --arg warehouse_id "$warehouse_id" \ - --arg catalog "$catalog" ' + --arg warehouse_id "$warehouse_id" ' .dashboard_id == $dashboard_id and .display_name == "Bakehouse Franchise Performance" and .warehouse_id == $warehouse_id - and .dataset_catalog == $catalog - and .dataset_schema == "bakehouse_gold" and (.serialized_dashboard | type == "string" and length > 0)' \ <<<"$draft" @@ -833,7 +841,7 @@ jq -e \ <<<"$published" ``` -Expected: draft metadata and serialized content match the requested resource, and the published object has the exact name, warehouse, and revision timestamp. +Expected: bundle summary proves the requested namespace, draft metadata and serialized content match the deployed resource, and the published object has the exact name, warehouse, and revision timestamp. Extract and assert the deployed serialized dashboard: @@ -909,8 +917,23 @@ jq -e ' and (.uiSettings.theme.canvasBackgroundColor.dark == "#111827") and (.uiSettings.theme.widgetBackgroundColor.light == "#FFFFFF") and (.uiSettings.theme.widgetBackgroundColor.dark == "#1F2937") + and (.uiSettings.theme.widgetBorderColor.light == "#FFFFFF") + and (.uiSettings.theme.widgetBorderColor.dark == "#1F2937") and (.uiSettings.theme.fontColor.light == "#172033") and (.uiSettings.theme.fontColor.dark == "#F3F4F6") + and (.uiSettings.theme.selectionColor.light == "#0072B2") + and (.uiSettings.theme.selectionColor.dark == "#56B4E9") + and (.uiSettings.theme.visualizationColors == [ + "#0072B2", + "#E69F00", + "#009E73", + "#CC79A7", + "#D55E00", + "#56B4E9" + ]) + and (.uiSettings.theme.widgetHeaderAlignment == "LEFT") + and (.uiSettings.theme.fontFamily == "Inter") + and (.uiSettings.theme.widgetCornerRadius == 8) ' "$deployed_dashboard" ``` From bd5c8bda1db305f2e4d0a67081dc13785899277a Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 11:52:23 -0600 Subject: [PATCH 03/13] fix: stop dashboard deployment on failed gates Keep the runbook in one strict shell so no later command can hide an earlier verification failure. --- .../content/02-databricks-projects/dashboards.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index 1ed412c..ffb3c44 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -48,11 +48,16 @@ Test every SQL statement before deployment. ## Run +Run every shell block in Run and Verify in the same Bash shell so resolved variables, helper functions, and fail-closed shell options persist. +With strict mode active, any failed auth check, strict validation, dataset assertion, bundle-summary assertion, publish assertion, draft assertion, published assertion, deployed structure assertion, or deployed dataset assertion stops the workflow. + ### 0. Verify auth and active-target inputs Require every human-provided input and fail closed if the profile reaches another target: ```bash +set -euo pipefail + : "${DATABRICKS_ACCOUNT_ID:?set the human-provided Databricks account ID}" : "${DATABRICKS_WORKSPACE_ID:?set the human-provided workspace ID}" : "${DATABRICKS_HOST:?set the human-provided workspace host}" From 7396d9a2ed6c4256275d136deb12277d851f5296 Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 12:02:52 -0600 Subject: [PATCH 04/13] fix: verify the effective dashboard name Distinguish the source-controlled display name from the development-prefixed name returned by deployed Lakeview resources. --- .../02-databricks-projects/dashboards.md | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index ffb3c44..7720d9a 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -746,13 +746,23 @@ resources: Strictly validate the local configuration: ```bash -databricks bundle validate \ - --strict \ - --target dev \ - --profile "$DATABRICKS_CONFIG_PROFILE" +validated_bundle=$( + databricks bundle validate \ + --strict \ + --target dev \ + --profile "$DATABRICKS_CONFIG_PROFILE" \ + -o json +) + +configured_display_name=$( + jq -er ' + .resources.dashboards.bakehouse_franchise_performance.display_name + | select(. == "Bakehouse Franchise Performance")' \ + <<<"$validated_bundle" +) ``` -Expected: strict validation succeeds with the dashboard resource key and all native fields. +Expected: strict validation succeeds and the configured dashboard resource display name is exactly `Bakehouse Franchise Performance`. ### 4. Deploy and publish @@ -793,7 +803,16 @@ dashboard_url=$( <<<"$summary" ) -printf 'dashboard_id=%s\ndashboard_url=%s\n' "$dashboard_id" "$dashboard_url" +effective_display_name=$( + jq -er ' + .resources.dashboards.bakehouse_franchise_performance.display_name + | select(type == "string" and length > 0) + | select(endswith("Bakehouse Franchise Performance"))' \ + <<<"$summary" +) + +printf 'configured_display_name=%s\neffective_display_name=%s\ndashboard_id=%s\ndashboard_url=%s\n' \ + "$configured_display_name" "$effective_display_name" "$dashboard_id" "$dashboard_url" published_after_publish=$( databricks lakeview publish "$dashboard_id" \ @@ -803,14 +822,15 @@ published_after_publish=$( ) jq -e \ + --arg effective_display_name "$effective_display_name" \ --arg warehouse_id "$warehouse_id" ' - .display_name == "Bakehouse Franchise Performance" + .display_name == $effective_display_name and .warehouse_id == $warehouse_id and (.revision_create_time | type == "string" and length > 0)' \ <<<"$published_after_publish" ``` -Expected: deployment succeeds, summary returns one nonempty dashboard ID and URL, and publish returns the exact display name, warehouse, and a revision timestamp. +Expected: deployment succeeds, summary returns one nonempty dashboard ID and URL, the effective name is nonempty and ends with `Bakehouse Franchise Performance`, and publish returns that effective name, the warehouse, and a revision timestamp. ## Verify @@ -831,22 +851,24 @@ published=$( jq -e \ --arg dashboard_id "$dashboard_id" \ + --arg effective_display_name "$effective_display_name" \ --arg warehouse_id "$warehouse_id" ' .dashboard_id == $dashboard_id - and .display_name == "Bakehouse Franchise Performance" + and .display_name == $effective_display_name and .warehouse_id == $warehouse_id and (.serialized_dashboard | type == "string" and length > 0)' \ <<<"$draft" jq -e \ + --arg effective_display_name "$effective_display_name" \ --arg warehouse_id "$warehouse_id" ' - .display_name == "Bakehouse Franchise Performance" + .display_name == $effective_display_name and .warehouse_id == $warehouse_id and (.revision_create_time | type == "string" and length > 0)' \ <<<"$published" ``` -Expected: bundle summary proves the requested namespace, draft metadata and serialized content match the deployed resource, and the published object has the exact name, warehouse, and revision timestamp. +Expected: bundle summary proves the requested namespace, draft metadata and serialized content use the effective deployed name, and the published object has that name, the warehouse, and a revision timestamp. Extract and assert the deployed serialized dashboard: @@ -967,6 +989,8 @@ No visual inspection may substitute for these executable checks. | A widget is invalid | A counter is not version 2 or a line or bar is not version 3 | Restore the exact widget versions | | A widget has no selected fields | A query field name differs from its encoding field name | Restore the exact locked field bindings | | Strict bundle validation fails | The native resource is malformed or its source path is wrong | Restore the exact resource and the `../src` path | +| Configured display-name assertion fails | The source-controlled resource name differs from `Bakehouse Franchise Performance` | Restore the exact configured `display_name` and strictly validate again | +| Effective display-name extraction fails | The deployed development name is empty or does not retain the configured name as its suffix | Inspect the target presets and require an effective name ending with `Bakehouse Franchise Performance` | | Dashboard ID extraction fails | Bundle summary lacks the exact resource key | Inspect deployment output and restore `bakehouse_franchise_performance` | | Publish fails or the published check fails | The principal cannot publish, the warehouse is wrong, or no published revision exists | Correct permission or warehouse access, republish the positional ID, and repeat both checks | | Deployed structure assertion fails | Server serialization or the deployed source differs from the locked contract | Compare the extracted object with the source, correct the source, and redeploy | From 447a826168730c74bea8cccf9d1a2536d69dd79a Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 12:06:28 -0600 Subject: [PATCH 05/13] fix: verify the source dashboard name before presets Read the configured display name from the resource file because bundle validation already applies development prefixes. --- .../02-databricks-projects/dashboards.md | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index 7720d9a..b7009f3 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -746,23 +746,19 @@ resources: Strictly validate the local configuration: ```bash -validated_bundle=$( - databricks bundle validate \ - --strict \ - --target dev \ - --profile "$DATABRICKS_CONFIG_PROFILE" \ - -o json -) +databricks bundle validate \ + --strict \ + --target dev \ + --profile "$DATABRICKS_CONFIG_PROFILE" \ + -o json >/dev/null -configured_display_name=$( - jq -er ' - .resources.dashboards.bakehouse_franchise_performance.display_name - | select(. == "Bakehouse Franchise Performance")' \ - <<<"$validated_bundle" -) +resource_file=resources/bakehouse_franchise_performance.dashboard.yml +rg -Fxq ' display_name: Bakehouse Franchise Performance' "$resource_file" +configured_display_name='Bakehouse Franchise Performance' ``` -Expected: strict validation succeeds and the configured dashboard resource display name is exactly `Bakehouse Franchise Performance`. +Expected: strict validation succeeds and the source YAML contains the exact configured display name `Bakehouse Franchise Performance`. +Development presets are already applied in validation and summary output, so only the source YAML proves the unprefixed configured value. ### 4. Deploy and publish @@ -989,7 +985,7 @@ No visual inspection may substitute for these executable checks. | A widget is invalid | A counter is not version 2 or a line or bar is not version 3 | Restore the exact widget versions | | A widget has no selected fields | A query field name differs from its encoding field name | Restore the exact locked field bindings | | Strict bundle validation fails | The native resource is malformed or its source path is wrong | Restore the exact resource and the `../src` path | -| Configured display-name assertion fails | The source-controlled resource name differs from `Bakehouse Franchise Performance` | Restore the exact configured `display_name` and strictly validate again | +| Configured display-name assertion fails | The source YAML does not contain the exact `display_name: Bakehouse Franchise Performance` line | Restore the exact configured line in `resources/bakehouse_franchise_performance.dashboard.yml` and rerun its `rg -Fxq` assertion | | Effective display-name extraction fails | The deployed development name is empty or does not retain the configured name as its suffix | Inspect the target presets and require an effective name ending with `Bakehouse Franchise Performance` | | Dashboard ID extraction fails | Bundle summary lacks the exact resource key | Inspect deployment output and restore `bakehouse_franchise_performance` | | Publish fails or the published check fails | The principal cannot publish, the warehouse is wrong, or no published revision exists | Correct permission or warehouse access, republish the positional ID, and repeat both checks | From b7cb51bb457ce243af8bc07800c3501a8aab9469 Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 12:11:37 -0600 Subject: [PATCH 06/13] docs: compact the dashboard definition Keep the complete copyable resource below Next.js page-data warning limits without changing its serialized value. --- .../02-databricks-projects/dashboards.md | 461 +----------------- 1 file changed, 1 insertion(+), 460 deletions(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index b7009f3..bed1b28 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -256,466 +256,7 @@ If the source data changes, stop and obtain human approval before changing the l Create `src/bakehouse_franchise_performance.lvdash.json` with this complete definition: ```json -{ - "datasets": [ - { - "name": "ds_kpis", - "displayName": "Franchise performance KPIs", - "queryLines": [ - "SELECT MEASURE(total_sales) AS total_sales,\n", - " MEASURE(order_count) AS order_count,\n", - " MEASURE(avg_order_value) AS avg_order_value\n", - "FROM franchise_sales_metrics " - ] - }, - { - "name": "ds_sales_trend", - "displayName": "Daily sales trend", - "queryLines": [ - "SELECT sales_date, MEASURE(total_sales) AS total_sales\n", - "FROM franchise_sales_metrics\n", - "GROUP BY sales_date\n", - "ORDER BY sales_date " - ] - }, - { - "name": "ds_franchises", - "displayName": "Top franchises", - "queryLines": [ - "SELECT franchise, MEASURE(total_sales) AS total_sales\n", - "FROM franchise_sales_metrics\n", - "GROUP BY franchise\n", - "ORDER BY total_sales DESC\n", - "LIMIT 10 " - ] - }, - { - "name": "ds_products", - "displayName": "Top products", - "queryLines": [ - "SELECT product, MEASURE(total_sales) AS total_sales\n", - "FROM franchise_sales_metrics\n", - "GROUP BY product\n", - "ORDER BY total_sales DESC\n", - "LIMIT 10 " - ] - } - ], - "pages": [ - { - "name": "overview", - "displayName": "Overview", - "pageType": "PAGE_TYPE_CANVAS", - "layoutVersion": "GRID_V1", - "layout": [ - { - "widget": { - "name": "dashboard-title", - "multilineTextboxSpec": { - "lines": [ - "# Bakehouse Franchise Performance" - ] - } - }, - "position": { - "x": 0, - "y": 0, - "width": 12, - "height": 1 - } - }, - { - "widget": { - "name": "dashboard-subtitle", - "multilineTextboxSpec": { - "lines": [ - "Daily sales, order volume, and product performance across the Bakehouse franchise network." - ] - } - }, - "position": { - "x": 0, - "y": 1, - "width": 12, - "height": 1 - } - }, - { - "widget": { - "name": "total-sales-kpi", - "queries": [ - { - "name": "main_query", - "query": { - "datasetName": "ds_kpis", - "fields": [ - { - "name": "total_sales", - "expression": "`total_sales`" - } - ], - "disaggregated": true - } - } - ], - "spec": { - "version": 2, - "widgetType": "counter", - "encodings": { - "value": { - "fieldName": "total_sales", - "displayName": "Total Sales", - "format": { - "type": "number-plain", - "abbreviation": "compact", - "decimalPlaces": { - "type": "max", - "places": 2 - } - } - } - }, - "frame": { - "showTitle": true, - "title": "Total Sales" - } - } - }, - "position": { - "x": 0, - "y": 2, - "width": 4, - "height": 3 - } - }, - { - "widget": { - "name": "order-count-kpi", - "queries": [ - { - "name": "main_query", - "query": { - "datasetName": "ds_kpis", - "fields": [ - { - "name": "order_count", - "expression": "`order_count`" - } - ], - "disaggregated": true - } - } - ], - "spec": { - "version": 2, - "widgetType": "counter", - "encodings": { - "value": { - "fieldName": "order_count", - "displayName": "Order Count", - "format": { - "type": "number-plain", - "decimalPlaces": { - "type": "exact", - "places": 0 - } - } - } - }, - "frame": { - "showTitle": true, - "title": "Order Count" - } - } - }, - "position": { - "x": 4, - "y": 2, - "width": 4, - "height": 3 - } - }, - { - "widget": { - "name": "average-order-value-kpi", - "queries": [ - { - "name": "main_query", - "query": { - "datasetName": "ds_kpis", - "fields": [ - { - "name": "avg_order_value", - "expression": "`avg_order_value`" - } - ], - "disaggregated": true - } - } - ], - "spec": { - "version": 2, - "widgetType": "counter", - "encodings": { - "value": { - "fieldName": "avg_order_value", - "displayName": "Average Order Value", - "format": { - "type": "number-plain", - "decimalPlaces": { - "type": "exact", - "places": 2 - } - } - } - }, - "frame": { - "showTitle": true, - "title": "Average Order Value" - } - } - }, - "position": { - "x": 8, - "y": 2, - "width": 4, - "height": 3 - } - }, - { - "widget": { - "name": "daily-sales-trend", - "queries": [ - { - "name": "main_query", - "query": { - "datasetName": "ds_sales_trend", - "fields": [ - { - "name": "sales_date", - "expression": "`sales_date`" - }, - { - "name": "total_sales", - "expression": "`total_sales`" - } - ], - "disaggregated": true - } - } - ], - "spec": { - "version": 3, - "widgetType": "line", - "encodings": { - "x": { - "fieldName": "sales_date", - "displayName": "Sales Date", - "scale": { - "type": "temporal" - } - }, - "y": { - "fieldName": "total_sales", - "displayName": "Total Sales", - "scale": { - "type": "quantitative", - "domainMin": 0 - }, - "format": { - "type": "number", - "abbreviation": "compact", - "decimalPlaces": { - "type": "max", - "places": 2 - } - } - } - }, - "frame": { - "showTitle": true, - "title": "Daily Sales Trend", - "showDescription": true, - "description": "Total sales by calendar day." - } - } - }, - "position": { - "x": 0, - "y": 5, - "width": 12, - "height": 6 - } - }, - { - "widget": { - "name": "top-franchises", - "queries": [ - { - "name": "main_query", - "query": { - "datasetName": "ds_franchises", - "fields": [ - { - "name": "franchise", - "expression": "`franchise`" - }, - { - "name": "total_sales", - "expression": "`total_sales`" - } - ], - "disaggregated": true - } - } - ], - "spec": { - "version": 3, - "widgetType": "bar", - "encodings": { - "x": { - "fieldName": "total_sales", - "displayName": "Total Sales", - "scale": { - "type": "quantitative", - "domainMin": 0 - }, - "format": { - "type": "number", - "abbreviation": "compact", - "decimalPlaces": { - "type": "max", - "places": 2 - } - } - }, - "y": { - "fieldName": "franchise", - "displayName": "Franchise", - "scale": { - "type": "categorical" - } - } - }, - "frame": { - "showTitle": true, - "title": "Top Franchises", - "showDescription": true, - "description": "Ten franchises with the highest total sales." - } - } - }, - "position": { - "x": 0, - "y": 11, - "width": 6, - "height": 6 - } - }, - { - "widget": { - "name": "top-products", - "queries": [ - { - "name": "main_query", - "query": { - "datasetName": "ds_products", - "fields": [ - { - "name": "product", - "expression": "`product`" - }, - { - "name": "total_sales", - "expression": "`total_sales`" - } - ], - "disaggregated": true - } - } - ], - "spec": { - "version": 3, - "widgetType": "bar", - "encodings": { - "x": { - "fieldName": "total_sales", - "displayName": "Total Sales", - "scale": { - "type": "quantitative", - "domainMin": 0 - }, - "format": { - "type": "number", - "abbreviation": "compact", - "decimalPlaces": { - "type": "max", - "places": 2 - } - } - }, - "y": { - "fieldName": "product", - "displayName": "Product", - "scale": { - "type": "categorical" - } - } - }, - "frame": { - "showTitle": true, - "title": "Top Products", - "showDescription": true, - "description": "Products ranked by total sales." - } - } - }, - "position": { - "x": 6, - "y": 11, - "width": 6, - "height": 6 - } - } - ] - } - ], - "uiSettings": { - "theme": { - "canvasBackgroundColor": { - "light": "#F5F7FA", - "dark": "#111827" - }, - "widgetBackgroundColor": { - "light": "#FFFFFF", - "dark": "#1F2937" - }, - "widgetBorderColor": { - "light": "#FFFFFF", - "dark": "#1F2937" - }, - "fontColor": { - "light": "#172033", - "dark": "#F3F4F6" - }, - "selectionColor": { - "light": "#0072B2", - "dark": "#56B4E9" - }, - "visualizationColors": [ - "#0072B2", - "#E69F00", - "#009E73", - "#CC79A7", - "#D55E00", - "#56B4E9" - ], - "widgetHeaderAlignment": "LEFT", - "fontFamily": "Inter", - "widgetCornerRadius": 8 - } - } -} +{"datasets":[{"name":"ds_kpis","displayName":"Franchise performance KPIs","queryLines":["SELECT MEASURE(total_sales) AS total_sales,\n"," MEASURE(order_count) AS order_count,\n"," MEASURE(avg_order_value) AS avg_order_value\n","FROM franchise_sales_metrics "]},{"name":"ds_sales_trend","displayName":"Daily sales trend","queryLines":["SELECT sales_date, MEASURE(total_sales) AS total_sales\n","FROM franchise_sales_metrics\n","GROUP BY sales_date\n","ORDER BY sales_date "]},{"name":"ds_franchises","displayName":"Top franchises","queryLines":["SELECT franchise, MEASURE(total_sales) AS total_sales\n","FROM franchise_sales_metrics\n","GROUP BY franchise\n","ORDER BY total_sales DESC\n","LIMIT 10 "]},{"name":"ds_products","displayName":"Top products","queryLines":["SELECT product, MEASURE(total_sales) AS total_sales\n","FROM franchise_sales_metrics\n","GROUP BY product\n","ORDER BY total_sales DESC\n","LIMIT 10 "]}],"pages":[{"name":"overview","displayName":"Overview","pageType":"PAGE_TYPE_CANVAS","layoutVersion":"GRID_V1","layout":[{"widget":{"name":"dashboard-title","multilineTextboxSpec":{"lines":["# Bakehouse Franchise Performance"]}},"position":{"x":0,"y":0,"width":12,"height":1}},{"widget":{"name":"dashboard-subtitle","multilineTextboxSpec":{"lines":["Daily sales, order volume, and product performance across the Bakehouse franchise network."]}},"position":{"x":0,"y":1,"width":12,"height":1}},{"widget":{"name":"total-sales-kpi","queries":[{"name":"main_query","query":{"datasetName":"ds_kpis","fields":[{"name":"total_sales","expression":"`total_sales`"}],"disaggregated":true}}],"spec":{"version":2,"widgetType":"counter","encodings":{"value":{"fieldName":"total_sales","displayName":"Total Sales","format":{"type":"number-plain","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}}},"frame":{"showTitle":true,"title":"Total Sales"}}},"position":{"x":0,"y":2,"width":4,"height":3}},{"widget":{"name":"order-count-kpi","queries":[{"name":"main_query","query":{"datasetName":"ds_kpis","fields":[{"name":"order_count","expression":"`order_count`"}],"disaggregated":true}}],"spec":{"version":2,"widgetType":"counter","encodings":{"value":{"fieldName":"order_count","displayName":"Order Count","format":{"type":"number-plain","decimalPlaces":{"type":"exact","places":0}}}},"frame":{"showTitle":true,"title":"Order Count"}}},"position":{"x":4,"y":2,"width":4,"height":3}},{"widget":{"name":"average-order-value-kpi","queries":[{"name":"main_query","query":{"datasetName":"ds_kpis","fields":[{"name":"avg_order_value","expression":"`avg_order_value`"}],"disaggregated":true}}],"spec":{"version":2,"widgetType":"counter","encodings":{"value":{"fieldName":"avg_order_value","displayName":"Average Order Value","format":{"type":"number-plain","decimalPlaces":{"type":"exact","places":2}}}},"frame":{"showTitle":true,"title":"Average Order Value"}}},"position":{"x":8,"y":2,"width":4,"height":3}},{"widget":{"name":"daily-sales-trend","queries":[{"name":"main_query","query":{"datasetName":"ds_sales_trend","fields":[{"name":"sales_date","expression":"`sales_date`"},{"name":"total_sales","expression":"`total_sales`"}],"disaggregated":true}}],"spec":{"version":3,"widgetType":"line","encodings":{"x":{"fieldName":"sales_date","displayName":"Sales Date","scale":{"type":"temporal"}},"y":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}}},"frame":{"showTitle":true,"title":"Daily Sales Trend","showDescription":true,"description":"Total sales by calendar day."}}},"position":{"x":0,"y":5,"width":12,"height":6}},{"widget":{"name":"top-franchises","queries":[{"name":"main_query","query":{"datasetName":"ds_franchises","fields":[{"name":"franchise","expression":"`franchise`"},{"name":"total_sales","expression":"`total_sales`"}],"disaggregated":true}}],"spec":{"version":3,"widgetType":"bar","encodings":{"x":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}},"y":{"fieldName":"franchise","displayName":"Franchise","scale":{"type":"categorical"}}},"frame":{"showTitle":true,"title":"Top Franchises","showDescription":true,"description":"Ten franchises with the highest total sales."}}},"position":{"x":0,"y":11,"width":6,"height":6}},{"widget":{"name":"top-products","queries":[{"name":"main_query","query":{"datasetName":"ds_products","fields":[{"name":"product","expression":"`product`"},{"name":"total_sales","expression":"`total_sales`"}],"disaggregated":true}}],"spec":{"version":3,"widgetType":"bar","encodings":{"x":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}},"y":{"fieldName":"product","displayName":"Product","scale":{"type":"categorical"}}},"frame":{"showTitle":true,"title":"Top Products","showDescription":true,"description":"Products ranked by total sales."}}},"position":{"x":6,"y":11,"width":6,"height":6}}]}],"uiSettings":{"theme":{"canvasBackgroundColor":{"light":"#F5F7FA","dark":"#111827"},"widgetBackgroundColor":{"light":"#FFFFFF","dark":"#1F2937"},"widgetBorderColor":{"light":"#FFFFFF","dark":"#1F2937"},"fontColor":{"light":"#172033","dark":"#F3F4F6"},"selectionColor":{"light":"#0072B2","dark":"#56B4E9"},"visualizationColors":["#0072B2","#E69F00","#009E73","#CC79A7","#D55E00","#56B4E9"],"widgetHeaderAlignment":"LEFT","fontFamily":"Inter","widgetCornerRadius":8}}} ``` Parse the source and execute all four exact queries before deployment: From ffa417b967ec8e5ba4cf810beea562d82df502e3 Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 12:21:00 -0600 Subject: [PATCH 07/13] docs: label dashboard proof and reject duplicates Make captured evidence readable and require the bundle deployment to own the only matching dashboard. --- .../02-databricks-projects/dashboards.md | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index bed1b28..8c25603 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -264,7 +264,8 @@ Parse the source and execute all four exact queries before deployment: ```bash dashboard_file=src/bakehouse_franchise_performance.lvdash.json jq -e '.' "$dashboard_file" >/dev/null -assert_all_datasets "$dashboard_file" +assert_all_datasets "$dashboard_file" >/dev/null +printf '%s\n' 'local_datasets=passed' ``` Expected: JSON parsing and all source dataset assertions succeed. @@ -325,7 +326,7 @@ jq -e \ and (.url | type == "string" and length > 0) and .dataset_catalog == $catalog and .dataset_schema == "bakehouse_gold"' \ - <<<"$summary" + <<<"$summary" >/dev/null dashboard_id=$( jq -er ' @@ -348,8 +349,23 @@ effective_display_name=$( <<<"$summary" ) +dashboards=$( + databricks lakeview list \ + --profile "$DATABRICKS_CONFIG_PROFILE" \ + -o json +) + +jq -e \ + --arg configured_display_name "$configured_display_name" \ + --arg dashboard_id "$dashboard_id" ' + [.[] | select((.display_name? // "") | endswith($configured_display_name))] + | length == 1 + and .[0].dashboard_id == $dashboard_id' \ + <<<"$dashboards" >/dev/null + printf 'configured_display_name=%s\neffective_display_name=%s\ndashboard_id=%s\ndashboard_url=%s\n' \ "$configured_display_name" "$effective_display_name" "$dashboard_id" "$dashboard_url" +printf '%s\n' 'bundle_summary_and_duplicate=passed' published_after_publish=$( databricks lakeview publish "$dashboard_id" \ @@ -364,10 +380,13 @@ jq -e \ .display_name == $effective_display_name and .warehouse_id == $warehouse_id and (.revision_create_time | type == "string" and length > 0)' \ - <<<"$published_after_publish" + <<<"$published_after_publish" >/dev/null + +printf '%s\n' 'publish=passed' ``` -Expected: deployment succeeds, summary returns one nonempty dashboard ID and URL, the effective name is nonempty and ends with `Bakehouse Franchise Performance`, and publish returns that effective name, the warehouse, and a revision timestamp. +Expected: deployment succeeds, summary returns one nonempty dashboard ID and URL, the effective name is nonempty and ends with `Bakehouse Franchise Performance`, exactly one matching dashboard has the bundle-summary ID, and publish returns that effective name, the warehouse, and a revision timestamp. +The list assertion detects duplicate or orphaned dashboards. ## Verify @@ -394,7 +413,7 @@ jq -e \ and .display_name == $effective_display_name and .warehouse_id == $warehouse_id and (.serialized_dashboard | type == "string" and length > 0)' \ - <<<"$draft" + <<<"$draft" >/dev/null jq -e \ --arg effective_display_name "$effective_display_name" \ @@ -402,7 +421,9 @@ jq -e \ .display_name == $effective_display_name and .warehouse_id == $warehouse_id and (.revision_create_time | type == "string" and length > 0)' \ - <<<"$published" + <<<"$published" >/dev/null + +printf '%s\n' 'draft_and_published=passed' ``` Expected: bundle summary proves the requested namespace, draft metadata and serialized content use the effective deployed name, and the published object has that name, the warehouse, and a revision timestamp. @@ -498,7 +519,9 @@ jq -e ' and (.uiSettings.theme.widgetHeaderAlignment == "LEFT") and (.uiSettings.theme.fontFamily == "Inter") and (.uiSettings.theme.widgetCornerRadius == 8) -' "$deployed_dashboard" +' "$deployed_dashboard" >/dev/null + +printf '%s\n' 'deployed_structure_and_theme=passed' ``` Expected: the deployed object has exactly four datasets, eight layout widgets, six exact business widgets, exact versions and bindings, bare metric-view references, no filters, no Genie link, and the complete light and dark theme. @@ -506,7 +529,8 @@ Expected: the deployed object has exactly four datasets, eight layout widgets, s Execute the deployed queries through the same API assertions: ```bash -assert_all_datasets "$deployed_dashboard" +assert_all_datasets "$deployed_dashboard" >/dev/null +printf '%s\n' 'deployed_datasets=passed' ``` Expected: all four deployed datasets satisfy the same row and value assertions as the source. @@ -529,6 +553,7 @@ No visual inspection may substitute for these executable checks. | Configured display-name assertion fails | The source YAML does not contain the exact `display_name: Bakehouse Franchise Performance` line | Restore the exact configured line in `resources/bakehouse_franchise_performance.dashboard.yml` and rerun its `rg -Fxq` assertion | | Effective display-name extraction fails | The deployed development name is empty or does not retain the configured name as its suffix | Inspect the target presets and require an effective name ending with `Bakehouse Franchise Performance` | | Dashboard ID extraction fails | Bundle summary lacks the exact resource key | Inspect deployment output and restore `bakehouse_franchise_performance` | +| Duplicate-dashboard assertion fails | More than one dashboard ends with the configured name or the only match has another ID | Remove or reconcile orphaned duplicates, then require the sole suffix match to equal the bundle-summary ID | | Publish fails or the published check fails | The principal cannot publish, the warehouse is wrong, or no published revision exists | Correct permission or warehouse access, republish the positional ID, and repeat both checks | | Deployed structure assertion fails | Server serialization or the deployed source differs from the locked contract | Compare the extracted object with the source, correct the source, and redeploy | | Deployed dataset assertion fails | Deployed SQL or source values differ from the verified local contract | Stop and reconcile the extracted queries and metric-view data | From 59c3f11e7475368a541445dd4851fa72ab481d60 Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 12:30:59 -0600 Subject: [PATCH 08/13] docs: prove dashboard updates retain identity Record asserted dataset values and fail if an existing bundle dashboard is replaced during deployment. --- .../02-databricks-projects/dashboards.md | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index 8c25603..d512275 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -184,6 +184,7 @@ assert_kpis() { | .[0] == 66471 and .[1] == 3333 and .[2] > 0 + and .[2] == 19.943294329432945 and ((.[0] - (.[1] * .[2])) > -0.01) and ((.[0] - (.[1] * .[2])) < 0.01) )' @@ -265,7 +266,7 @@ Parse the source and execute all four exact queries before deployment: dashboard_file=src/bakehouse_franchise_performance.lvdash.json jq -e '.' "$dashboard_file" >/dev/null assert_all_datasets "$dashboard_file" >/dev/null -printf '%s\n' 'local_datasets=passed' +printf '%s\n' 'local_datasets=passed kpis=66471,3333,19.943294329432945 trend=rows:17,first:2024-05-01:4128,last:2024-05-17:1932,total:66471 franchises=rows:10,leader:Baked Bliss:6642 products=rows:6,leader:Golden Gate Ginger:11595' ``` Expected: JSON parsing and all source dataset assertions succeed. @@ -307,6 +308,21 @@ Development presets are already applied in validation and summary output, so onl Deploy, resolve the dashboard ID only through bundle summary, and publish with positional CLI syntax: ```bash +pre_deploy_summary=$( + databricks bundle summary \ + --target dev \ + --profile "$DATABRICKS_CONFIG_PROFILE" \ + -o json +) + +pre_existing_dashboard_id=$( + jq -r ' + .resources.dashboards.bakehouse_franchise_performance.id + // empty + | tostring' \ + <<<"$pre_deploy_summary" +) + databricks bundle deploy \ --target dev \ --profile "$DATABRICKS_CONFIG_PROFILE" @@ -349,6 +365,11 @@ effective_display_name=$( <<<"$summary" ) +if test -n "$pre_existing_dashboard_id" +then + test "$pre_existing_dashboard_id" = "$dashboard_id" +fi + dashboards=$( databricks lakeview list \ --profile "$DATABRICKS_CONFIG_PROFILE" \ @@ -363,9 +384,10 @@ jq -e \ and .[0].dashboard_id == $dashboard_id' \ <<<"$dashboards" >/dev/null -printf 'configured_display_name=%s\neffective_display_name=%s\ndashboard_id=%s\ndashboard_url=%s\n' \ - "$configured_display_name" "$effective_display_name" "$dashboard_id" "$dashboard_url" -printf '%s\n' 'bundle_summary_and_duplicate=passed' +printf 'configured_display_name=%s\neffective_display_name=%s\ndashboard_url=%s\n' \ + "$configured_display_name" "$effective_display_name" "$dashboard_url" +printf 'bundle_summary_and_duplicate=passed pre_existing_dashboard_id=%s deployed_dashboard_id=%s matching_dashboards=1 duplicate_dashboards=0\n' \ + "$pre_existing_dashboard_id" "$dashboard_id" published_after_publish=$( databricks lakeview publish "$dashboard_id" \ @@ -386,6 +408,7 @@ printf '%s\n' 'publish=passed' ``` Expected: deployment succeeds, summary returns one nonempty dashboard ID and URL, the effective name is nonempty and ends with `Bakehouse Franchise Performance`, exactly one matching dashboard has the bundle-summary ID, and publish returns that effective name, the warehouse, and a revision timestamp. +The pre-existing ID may be empty on first creation, but a nonempty value must equal the deployed ID. The list assertion detects duplicate or orphaned dashboards. ## Verify @@ -530,7 +553,7 @@ Execute the deployed queries through the same API assertions: ```bash assert_all_datasets "$deployed_dashboard" >/dev/null -printf '%s\n' 'deployed_datasets=passed' +printf '%s\n' 'deployed_datasets=passed kpis=66471,3333,19.943294329432945 trend=rows:17,first:2024-05-01:4128,last:2024-05-17:1932,total:66471 franchises=rows:10,leader:Baked Bliss:6642 products=rows:6,leader:Golden Gate Ginger:11595' ``` Expected: all four deployed datasets satisfy the same row and value assertions as the source. @@ -553,6 +576,7 @@ No visual inspection may substitute for these executable checks. | Configured display-name assertion fails | The source YAML does not contain the exact `display_name: Bakehouse Franchise Performance` line | Restore the exact configured line in `resources/bakehouse_franchise_performance.dashboard.yml` and rerun its `rg -Fxq` assertion | | Effective display-name extraction fails | The deployed development name is empty or does not retain the configured name as its suffix | Inspect the target presets and require an effective name ending with `Bakehouse Franchise Performance` | | Dashboard ID extraction fails | Bundle summary lacks the exact resource key | Inspect deployment output and restore `bakehouse_franchise_performance` | +| Dashboard identity-retention assertion fails | Deployment replaced an existing bundle-managed dashboard with a new ID | Stop and reconcile bundle state so updates retain the pre-existing dashboard ID | | Duplicate-dashboard assertion fails | More than one dashboard ends with the configured name or the only match has another ID | Remove or reconcile orphaned duplicates, then require the sole suffix match to equal the bundle-summary ID | | Publish fails or the published check fails | The principal cannot publish, the warehouse is wrong, or no published revision exists | Correct permission or warehouse access, republish the positional ID, and repeat both checks | | Deployed structure assertion fails | Server serialization or the deployed source differs from the locked contract | Compare the extracted object with the source, correct the source, and redeploy | From eb22cc47204229735d8faf52abcade01de5309f1 Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 12:39:33 -0600 Subject: [PATCH 09/13] fix: make dashboard deployment non-interactive --- .../content/02-databricks-projects/dashboards.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index d512275..158b764 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -325,7 +325,8 @@ pre_existing_dashboard_id=$( databricks bundle deploy \ --target dev \ - --profile "$DATABRICKS_CONFIG_PROFILE" + --profile "$DATABRICKS_CONFIG_PROFILE" \ + --auto-approve summary=$( databricks bundle summary \ From 0fd2cb7bdfeff5cbce2e84fe6d196a6e8d8d2c3e Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 12:49:11 -0600 Subject: [PATCH 10/13] docs: strengthen dashboard contract verification --- .../02-databricks-projects/dashboards.md | 132 ++++++++++++------ 1 file changed, 90 insertions(+), 42 deletions(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index 158b764..e3ac0ec 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -9,6 +9,7 @@ description: Create, publish, and verify the Bakehouse Franchise Performance das An AI/BI dashboard is a native DABs resource backed by a serialized `.lvdash.json` definition. This page consumes the successful [Metric Views](/docs/02-databricks-projects/metric-views/) outcome. Dataset SQL remains portable by using bare `franchise_sales_metrics` and putting the catalog and schema on the native resource. +In development mode, validation can prefix the configured display name, so the resource YAML proves the configured name and bundle summary supplies the effective name used by Lakeview APIs. ## Goal @@ -49,7 +50,6 @@ Test every SQL statement before deployment. ## Run Run every shell block in Run and Verify in the same Bash shell so resolved variables, helper functions, and fail-closed shell options persist. -With strict mode active, any failed auth check, strict validation, dataset assertion, bundle-summary assertion, publish assertion, draft assertion, published assertion, deployed structure assertion, or deployed dataset assertion stops the workflow. ### 0. Verify auth and active-target inputs @@ -183,7 +183,6 @@ assert_kpis() { .result.data_array[0] | map(tonumber) | .[0] == 66471 and .[1] == 3333 - and .[2] > 0 and .[2] == 19.943294329432945 and ((.[0] - (.[1] * .[2])) > -0.01) and ((.[0] - (.[1] * .[2])) < 0.01) @@ -233,7 +232,6 @@ assert_products() { assert_all_datasets() { local dashboard_file=$1 sql - set -o pipefail sql=$(dataset_sql "$dashboard_file" ds_kpis) || return run_sql "$sql" | assert_kpis || return @@ -336,11 +334,13 @@ summary=$( ) jq -e \ - --arg catalog "$catalog" ' + --arg catalog "$catalog" \ + --arg warehouse_id "$warehouse_id" ' .resources.dashboards.bakehouse_franchise_performance | .id != null and (.id | tostring | length > 0) and (.url | type == "string" and length > 0) + and .warehouse_id == $warehouse_id and .dataset_catalog == $catalog and .dataset_schema == "bakehouse_gold"' \ <<<"$summary" >/dev/null @@ -461,61 +461,107 @@ trap 'rm -f "$deployed_dashboard"' EXIT jq -er '.serialized_dashboard | fromjson' <<<"$draft" >"$deployed_dashboard" jq -e ' - ([.datasets[].name] == [ - "ds_kpis", - "ds_sales_trend", - "ds_franchises", - "ds_products" + ([.datasets[] | {name, displayName}] == [ + {"name":"ds_kpis","displayName":"Franchise performance KPIs"}, + {"name":"ds_sales_trend","displayName":"Daily sales trend"}, + {"name":"ds_franchises","displayName":"Top franchises"}, + {"name":"ds_products","displayName":"Top products"} ]) - and (.datasets | length == 4) and (.pages | length == 1) and (.pages[0].name == "overview") + and (.pages[0].displayName == "Overview") and (.pages[0].pageType == "PAGE_TYPE_CANVAS") and (.pages[0].layoutVersion == "GRID_V1") and (.pages[0].layout | length == 8) and ( [ - .pages[0].layout[].widget - | select(.spec != null) + .pages[0].layout[] + | select(.widget.multilineTextboxSpec != null) | { - title: .spec.frame.title, - type: .spec.widgetType, - version: .spec.version + name: .widget.name, + lines: .widget.multilineTextboxSpec.lines, + position } ] == [ - {"title":"Total Sales","type":"counter","version":2}, - {"title":"Order Count","type":"counter","version":2}, - {"title":"Average Order Value","type":"counter","version":2}, - {"title":"Daily Sales Trend","type":"line","version":3}, - {"title":"Top Franchises","type":"bar","version":3}, - {"title":"Top Products","type":"bar","version":3} + {"name":"dashboard-title","lines":["# Bakehouse Franchise Performance"],"position":{"x":0,"y":0,"width":12,"height":1}}, + {"name":"dashboard-subtitle","lines":["Daily sales, order volume, and product performance across the Bakehouse franchise network."],"position":{"x":0,"y":1,"width":12,"height":1}} ] ) and ( [ - .pages[0].layout[].widget - | select(.spec != null) + .pages[0].layout[] + | select(.widget.spec != null) | { - name, - dataset: .queries[0].query.datasetName, - fields: [.queries[0].query.fields[].name], - encodings: ( - .spec.encodings - | [ - .value.fieldName?, - .x.fieldName?, - .y.fieldName? - ] - | map(select(. != null)) - ) + name: .widget.name, + title: .widget.spec.frame.title, + type: .widget.spec.widgetType, + version: .widget.spec.version, + dataset: .widget.queries[0].query.datasetName, + fields: .widget.queries[0].query.fields, + encodings: .widget.spec.encodings, + position } ] == [ - {"name":"total-sales-kpi","dataset":"ds_kpis","fields":["total_sales"],"encodings":["total_sales"]}, - {"name":"order-count-kpi","dataset":"ds_kpis","fields":["order_count"],"encodings":["order_count"]}, - {"name":"average-order-value-kpi","dataset":"ds_kpis","fields":["avg_order_value"],"encodings":["avg_order_value"]}, - {"name":"daily-sales-trend","dataset":"ds_sales_trend","fields":["sales_date","total_sales"],"encodings":["sales_date","total_sales"]}, - {"name":"top-franchises","dataset":"ds_franchises","fields":["franchise","total_sales"],"encodings":["total_sales","franchise"]}, - {"name":"top-products","dataset":"ds_products","fields":["product","total_sales"],"encodings":["total_sales","product"]} + { + "name":"total-sales-kpi", + "title":"Total Sales", + "type":"counter", + "version":2, + "dataset":"ds_kpis", + "fields":[{"name":"total_sales","expression":"`total_sales`"}], + "encodings":{"value":{"fieldName":"total_sales","displayName":"Total Sales","format":{"type":"number-plain","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}}}, + "position":{"x":0,"y":2,"width":4,"height":3} + }, + { + "name":"order-count-kpi", + "title":"Order Count", + "type":"counter", + "version":2, + "dataset":"ds_kpis", + "fields":[{"name":"order_count","expression":"`order_count`"}], + "encodings":{"value":{"fieldName":"order_count","displayName":"Order Count","format":{"type":"number-plain","decimalPlaces":{"type":"exact","places":0}}}}, + "position":{"x":4,"y":2,"width":4,"height":3} + }, + { + "name":"average-order-value-kpi", + "title":"Average Order Value", + "type":"counter", + "version":2, + "dataset":"ds_kpis", + "fields":[{"name":"avg_order_value","expression":"`avg_order_value`"}], + "encodings":{"value":{"fieldName":"avg_order_value","displayName":"Average Order Value","format":{"type":"number-plain","decimalPlaces":{"type":"exact","places":2}}}}, + "position":{"x":8,"y":2,"width":4,"height":3} + }, + { + "name":"daily-sales-trend", + "title":"Daily Sales Trend", + "type":"line", + "version":3, + "dataset":"ds_sales_trend", + "fields":[{"name":"sales_date","expression":"`sales_date`"},{"name":"total_sales","expression":"`total_sales`"}], + "encodings":{"x":{"fieldName":"sales_date","displayName":"Sales Date","scale":{"type":"temporal"}},"y":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}}}, + "position":{"x":0,"y":5,"width":12,"height":6} + }, + { + "name":"top-franchises", + "title":"Top Franchises", + "type":"bar", + "version":3, + "dataset":"ds_franchises", + "fields":[{"name":"franchise","expression":"`franchise`"},{"name":"total_sales","expression":"`total_sales`"}], + "encodings":{"x":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}},"y":{"fieldName":"franchise","displayName":"Franchise","scale":{"type":"categorical"}}}, + "position":{"x":0,"y":11,"width":6,"height":6} + }, + { + "name":"top-products", + "title":"Top Products", + "type":"bar", + "version":3, + "dataset":"ds_products", + "fields":[{"name":"product","expression":"`product`"},{"name":"total_sales","expression":"`total_sales`"}], + "encodings":{"x":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}},"y":{"fieldName":"product","displayName":"Product","scale":{"type":"categorical"}}}, + "position":{"x":6,"y":11,"width":6,"height":6} + } ] ) and ([.datasets[].queryLines | join("") | contains("FROM franchise_sales_metrics")] | all) @@ -575,10 +621,12 @@ No visual inspection may substitute for these executable checks. | A widget has no selected fields | A query field name differs from its encoding field name | Restore the exact locked field bindings | | Strict bundle validation fails | The native resource is malformed or its source path is wrong | Restore the exact resource and the `../src` path | | Configured display-name assertion fails | The source YAML does not contain the exact `display_name: Bakehouse Franchise Performance` line | Restore the exact configured line in `resources/bakehouse_franchise_performance.dashboard.yml` and rerun its `rg -Fxq` assertion | +| Validation reports an unexpected display-name prefix | The active development preset differs from the expected target configuration | Inspect the validation output and target preset, keep the source YAML unprefixed, and use bundle summary as the authority for the effective Lakeview API name | | Effective display-name extraction fails | The deployed development name is empty or does not retain the configured name as its suffix | Inspect the target presets and require an effective name ending with `Bakehouse Franchise Performance` | | Dashboard ID extraction fails | Bundle summary lacks the exact resource key | Inspect deployment output and restore `bakehouse_franchise_performance` | | Dashboard identity-retention assertion fails | Deployment replaced an existing bundle-managed dashboard with a new ID | Stop and reconcile bundle state so updates retain the pre-existing dashboard ID | -| Duplicate-dashboard assertion fails | More than one dashboard ends with the configured name or the only match has another ID | Remove or reconcile orphaned duplicates, then require the sole suffix match to equal the bundle-summary ID | +| Duplicate-dashboard assertion fails | More than one dashboard ends with the configured name or the only match has another ID | Use bundle summary to identify the bundle-owned dashboard, reconcile only dashboards whose ownership is proven, leave unproven suffix matches unchanged, and require the sole suffix match to equal the bundle-summary ID | +| Publish, draft GET, or published GET returns another display name | Lakeview state is stale or the response was compared with the configured name instead of the effective name | Refresh bundle summary, derive `effective_display_name` again, and require publish, get, and get-published to return that exact value before continuing | | Publish fails or the published check fails | The principal cannot publish, the warehouse is wrong, or no published revision exists | Correct permission or warehouse access, republish the positional ID, and repeat both checks | | Deployed structure assertion fails | Server serialization or the deployed source differs from the locked contract | Compare the extracted object with the source, correct the source, and redeploy | | Deployed dataset assertion fails | Deployed SQL or source values differ from the verified local contract | Stop and reconcile the extracted queries and metric-view data | From 65605b403fef6a3b725d2f3e82765216e0df4caf Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 12:56:10 -0600 Subject: [PATCH 11/13] docs: verify the complete dashboard serialization --- .../02-databricks-projects/dashboards.md | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index e3ac0ec..f9a870a 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -461,11 +461,17 @@ trap 'rm -f "$deployed_dashboard"' EXIT jq -er '.serialized_dashboard | fromjson' <<<"$draft" >"$deployed_dashboard" jq -e ' - ([.datasets[] | {name, displayName}] == [ - {"name":"ds_kpis","displayName":"Franchise performance KPIs"}, - {"name":"ds_sales_trend","displayName":"Daily sales trend"}, - {"name":"ds_franchises","displayName":"Top franchises"}, - {"name":"ds_products","displayName":"Top products"} + ([.datasets[] | {name, displayName, queryLines}] == [ + {"name":"ds_kpis","displayName":"Franchise performance KPIs","queryLines":["SELECT MEASURE(total_sales) AS total_sales,\n"," MEASURE(order_count) AS order_count,\n"," MEASURE(avg_order_value) AS avg_order_value\n","FROM franchise_sales_metrics "]}, + {"name":"ds_sales_trend","displayName":"Daily sales trend","queryLines":["SELECT sales_date, MEASURE(total_sales) AS total_sales\n","FROM franchise_sales_metrics\n","GROUP BY sales_date\n","ORDER BY sales_date "]}, + {"name":"ds_franchises","displayName":"Top franchises","queryLines":["SELECT franchise, MEASURE(total_sales) AS total_sales\n","FROM franchise_sales_metrics\n","GROUP BY franchise\n","ORDER BY total_sales DESC\n","LIMIT 10 "]}, + {"name":"ds_products","displayName":"Top products","queryLines":["SELECT product, MEASURE(total_sales) AS total_sales\n","FROM franchise_sales_metrics\n","GROUP BY product\n","ORDER BY total_sales DESC\n","LIMIT 10 "]} + ]) + and ([.datasets[].queryLines | join("")] == [ + "SELECT MEASURE(total_sales) AS total_sales,\n MEASURE(order_count) AS order_count,\n MEASURE(avg_order_value) AS avg_order_value\nFROM franchise_sales_metrics ", + "SELECT sales_date, MEASURE(total_sales) AS total_sales\nFROM franchise_sales_metrics\nGROUP BY sales_date\nORDER BY sales_date ", + "SELECT franchise, MEASURE(total_sales) AS total_sales\nFROM franchise_sales_metrics\nGROUP BY franchise\nORDER BY total_sales DESC\nLIMIT 10 ", + "SELECT product, MEASURE(total_sales) AS total_sales\nFROM franchise_sales_metrics\nGROUP BY product\nORDER BY total_sales DESC\nLIMIT 10 " ]) and (.pages | length == 1) and (.pages[0].name == "overview") @@ -494,9 +500,13 @@ jq -e ' | { name: .widget.name, title: .widget.spec.frame.title, + showTitle: .widget.spec.frame.showTitle, + description: (.widget.spec.frame.description // null), type: .widget.spec.widgetType, version: .widget.spec.version, + queryName: .widget.queries[0].name, dataset: .widget.queries[0].query.datasetName, + disaggregated: .widget.queries[0].query.disaggregated, fields: .widget.queries[0].query.fields, encodings: .widget.spec.encodings, position @@ -505,9 +515,13 @@ jq -e ' { "name":"total-sales-kpi", "title":"Total Sales", + "showTitle":true, + "description":null, "type":"counter", "version":2, + "queryName":"main_query", "dataset":"ds_kpis", + "disaggregated":true, "fields":[{"name":"total_sales","expression":"`total_sales`"}], "encodings":{"value":{"fieldName":"total_sales","displayName":"Total Sales","format":{"type":"number-plain","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}}}, "position":{"x":0,"y":2,"width":4,"height":3} @@ -515,9 +529,13 @@ jq -e ' { "name":"order-count-kpi", "title":"Order Count", + "showTitle":true, + "description":null, "type":"counter", "version":2, + "queryName":"main_query", "dataset":"ds_kpis", + "disaggregated":true, "fields":[{"name":"order_count","expression":"`order_count`"}], "encodings":{"value":{"fieldName":"order_count","displayName":"Order Count","format":{"type":"number-plain","decimalPlaces":{"type":"exact","places":0}}}}, "position":{"x":4,"y":2,"width":4,"height":3} @@ -525,9 +543,13 @@ jq -e ' { "name":"average-order-value-kpi", "title":"Average Order Value", + "showTitle":true, + "description":null, "type":"counter", "version":2, + "queryName":"main_query", "dataset":"ds_kpis", + "disaggregated":true, "fields":[{"name":"avg_order_value","expression":"`avg_order_value`"}], "encodings":{"value":{"fieldName":"avg_order_value","displayName":"Average Order Value","format":{"type":"number-plain","decimalPlaces":{"type":"exact","places":2}}}}, "position":{"x":8,"y":2,"width":4,"height":3} @@ -535,9 +557,13 @@ jq -e ' { "name":"daily-sales-trend", "title":"Daily Sales Trend", + "showTitle":true, + "description":"Total sales by calendar day.", "type":"line", "version":3, + "queryName":"main_query", "dataset":"ds_sales_trend", + "disaggregated":true, "fields":[{"name":"sales_date","expression":"`sales_date`"},{"name":"total_sales","expression":"`total_sales`"}], "encodings":{"x":{"fieldName":"sales_date","displayName":"Sales Date","scale":{"type":"temporal"}},"y":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}}}, "position":{"x":0,"y":5,"width":12,"height":6} @@ -545,9 +571,13 @@ jq -e ' { "name":"top-franchises", "title":"Top Franchises", + "showTitle":true, + "description":"Ten franchises with the highest total sales.", "type":"bar", "version":3, + "queryName":"main_query", "dataset":"ds_franchises", + "disaggregated":true, "fields":[{"name":"franchise","expression":"`franchise`"},{"name":"total_sales","expression":"`total_sales`"}], "encodings":{"x":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}},"y":{"fieldName":"franchise","displayName":"Franchise","scale":{"type":"categorical"}}}, "position":{"x":0,"y":11,"width":6,"height":6} @@ -555,9 +585,13 @@ jq -e ' { "name":"top-products", "title":"Top Products", + "showTitle":true, + "description":"Products ranked by total sales.", "type":"bar", "version":3, + "queryName":"main_query", "dataset":"ds_products", + "disaggregated":true, "fields":[{"name":"product","expression":"`product`"},{"name":"total_sales","expression":"`total_sales`"}], "encodings":{"x":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}},"y":{"fieldName":"product","displayName":"Product","scale":{"type":"categorical"}}}, "position":{"x":6,"y":11,"width":6,"height":6} From a2ee9d839caa3f37845c01928181033dcadf2cf0 Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 12:58:18 -0600 Subject: [PATCH 12/13] docs: simplify exact dashboard serialization proof --- .../02-databricks-projects/dashboards.md | 168 +----------------- 1 file changed, 2 insertions(+), 166 deletions(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index f9a870a..acbc52b 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -460,175 +460,11 @@ trap 'rm -f "$deployed_dashboard"' EXIT jq -er '.serialized_dashboard | fromjson' <<<"$draft" >"$deployed_dashboard" -jq -e ' - ([.datasets[] | {name, displayName, queryLines}] == [ - {"name":"ds_kpis","displayName":"Franchise performance KPIs","queryLines":["SELECT MEASURE(total_sales) AS total_sales,\n"," MEASURE(order_count) AS order_count,\n"," MEASURE(avg_order_value) AS avg_order_value\n","FROM franchise_sales_metrics "]}, - {"name":"ds_sales_trend","displayName":"Daily sales trend","queryLines":["SELECT sales_date, MEASURE(total_sales) AS total_sales\n","FROM franchise_sales_metrics\n","GROUP BY sales_date\n","ORDER BY sales_date "]}, - {"name":"ds_franchises","displayName":"Top franchises","queryLines":["SELECT franchise, MEASURE(total_sales) AS total_sales\n","FROM franchise_sales_metrics\n","GROUP BY franchise\n","ORDER BY total_sales DESC\n","LIMIT 10 "]}, - {"name":"ds_products","displayName":"Top products","queryLines":["SELECT product, MEASURE(total_sales) AS total_sales\n","FROM franchise_sales_metrics\n","GROUP BY product\n","ORDER BY total_sales DESC\n","LIMIT 10 "]} - ]) - and ([.datasets[].queryLines | join("")] == [ - "SELECT MEASURE(total_sales) AS total_sales,\n MEASURE(order_count) AS order_count,\n MEASURE(avg_order_value) AS avg_order_value\nFROM franchise_sales_metrics ", - "SELECT sales_date, MEASURE(total_sales) AS total_sales\nFROM franchise_sales_metrics\nGROUP BY sales_date\nORDER BY sales_date ", - "SELECT franchise, MEASURE(total_sales) AS total_sales\nFROM franchise_sales_metrics\nGROUP BY franchise\nORDER BY total_sales DESC\nLIMIT 10 ", - "SELECT product, MEASURE(total_sales) AS total_sales\nFROM franchise_sales_metrics\nGROUP BY product\nORDER BY total_sales DESC\nLIMIT 10 " - ]) - and (.pages | length == 1) - and (.pages[0].name == "overview") - and (.pages[0].displayName == "Overview") - and (.pages[0].pageType == "PAGE_TYPE_CANVAS") - and (.pages[0].layoutVersion == "GRID_V1") - and (.pages[0].layout | length == 8) - and ( - [ - .pages[0].layout[] - | select(.widget.multilineTextboxSpec != null) - | { - name: .widget.name, - lines: .widget.multilineTextboxSpec.lines, - position - } - ] == [ - {"name":"dashboard-title","lines":["# Bakehouse Franchise Performance"],"position":{"x":0,"y":0,"width":12,"height":1}}, - {"name":"dashboard-subtitle","lines":["Daily sales, order volume, and product performance across the Bakehouse franchise network."],"position":{"x":0,"y":1,"width":12,"height":1}} - ] - ) - and ( - [ - .pages[0].layout[] - | select(.widget.spec != null) - | { - name: .widget.name, - title: .widget.spec.frame.title, - showTitle: .widget.spec.frame.showTitle, - description: (.widget.spec.frame.description // null), - type: .widget.spec.widgetType, - version: .widget.spec.version, - queryName: .widget.queries[0].name, - dataset: .widget.queries[0].query.datasetName, - disaggregated: .widget.queries[0].query.disaggregated, - fields: .widget.queries[0].query.fields, - encodings: .widget.spec.encodings, - position - } - ] == [ - { - "name":"total-sales-kpi", - "title":"Total Sales", - "showTitle":true, - "description":null, - "type":"counter", - "version":2, - "queryName":"main_query", - "dataset":"ds_kpis", - "disaggregated":true, - "fields":[{"name":"total_sales","expression":"`total_sales`"}], - "encodings":{"value":{"fieldName":"total_sales","displayName":"Total Sales","format":{"type":"number-plain","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}}}, - "position":{"x":0,"y":2,"width":4,"height":3} - }, - { - "name":"order-count-kpi", - "title":"Order Count", - "showTitle":true, - "description":null, - "type":"counter", - "version":2, - "queryName":"main_query", - "dataset":"ds_kpis", - "disaggregated":true, - "fields":[{"name":"order_count","expression":"`order_count`"}], - "encodings":{"value":{"fieldName":"order_count","displayName":"Order Count","format":{"type":"number-plain","decimalPlaces":{"type":"exact","places":0}}}}, - "position":{"x":4,"y":2,"width":4,"height":3} - }, - { - "name":"average-order-value-kpi", - "title":"Average Order Value", - "showTitle":true, - "description":null, - "type":"counter", - "version":2, - "queryName":"main_query", - "dataset":"ds_kpis", - "disaggregated":true, - "fields":[{"name":"avg_order_value","expression":"`avg_order_value`"}], - "encodings":{"value":{"fieldName":"avg_order_value","displayName":"Average Order Value","format":{"type":"number-plain","decimalPlaces":{"type":"exact","places":2}}}}, - "position":{"x":8,"y":2,"width":4,"height":3} - }, - { - "name":"daily-sales-trend", - "title":"Daily Sales Trend", - "showTitle":true, - "description":"Total sales by calendar day.", - "type":"line", - "version":3, - "queryName":"main_query", - "dataset":"ds_sales_trend", - "disaggregated":true, - "fields":[{"name":"sales_date","expression":"`sales_date`"},{"name":"total_sales","expression":"`total_sales`"}], - "encodings":{"x":{"fieldName":"sales_date","displayName":"Sales Date","scale":{"type":"temporal"}},"y":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}}}, - "position":{"x":0,"y":5,"width":12,"height":6} - }, - { - "name":"top-franchises", - "title":"Top Franchises", - "showTitle":true, - "description":"Ten franchises with the highest total sales.", - "type":"bar", - "version":3, - "queryName":"main_query", - "dataset":"ds_franchises", - "disaggregated":true, - "fields":[{"name":"franchise","expression":"`franchise`"},{"name":"total_sales","expression":"`total_sales`"}], - "encodings":{"x":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}},"y":{"fieldName":"franchise","displayName":"Franchise","scale":{"type":"categorical"}}}, - "position":{"x":0,"y":11,"width":6,"height":6} - }, - { - "name":"top-products", - "title":"Top Products", - "showTitle":true, - "description":"Products ranked by total sales.", - "type":"bar", - "version":3, - "queryName":"main_query", - "dataset":"ds_products", - "disaggregated":true, - "fields":[{"name":"product","expression":"`product`"},{"name":"total_sales","expression":"`total_sales`"}], - "encodings":{"x":{"fieldName":"total_sales","displayName":"Total Sales","scale":{"type":"quantitative","domainMin":0},"format":{"type":"number","abbreviation":"compact","decimalPlaces":{"type":"max","places":2}}},"y":{"fieldName":"product","displayName":"Product","scale":{"type":"categorical"}}}, - "position":{"x":6,"y":11,"width":6,"height":6} - } - ] - ) - and ([.datasets[].queryLines | join("") | contains("FROM franchise_sales_metrics")] | all) - and ([.datasets[].queryLines | join("") | test("FROM[[:space:]]+[^[:space:]]*\\.[^[:space:]]*franchise_sales_metrics")] | any | not) - and ([.pages[].layout[].widget.spec.widgetType? | select(. != null) | startswith("filter")] | any | not) - and (.uiSettings.genieSpace? == null) - and (.uiSettings.theme.canvasBackgroundColor.light == "#F5F7FA") - and (.uiSettings.theme.canvasBackgroundColor.dark == "#111827") - and (.uiSettings.theme.widgetBackgroundColor.light == "#FFFFFF") - and (.uiSettings.theme.widgetBackgroundColor.dark == "#1F2937") - and (.uiSettings.theme.widgetBorderColor.light == "#FFFFFF") - and (.uiSettings.theme.widgetBorderColor.dark == "#1F2937") - and (.uiSettings.theme.fontColor.light == "#172033") - and (.uiSettings.theme.fontColor.dark == "#F3F4F6") - and (.uiSettings.theme.selectionColor.light == "#0072B2") - and (.uiSettings.theme.selectionColor.dark == "#56B4E9") - and (.uiSettings.theme.visualizationColors == [ - "#0072B2", - "#E69F00", - "#009E73", - "#CC79A7", - "#D55E00", - "#56B4E9" - ]) - and (.uiSettings.theme.widgetHeaderAlignment == "LEFT") - and (.uiSettings.theme.fontFamily == "Inter") - and (.uiSettings.theme.widgetCornerRadius == 8) -' "$deployed_dashboard" >/dev/null - +jq -e --slurpfile source src/bakehouse_franchise_performance.lvdash.json '. == $source[0]' "$deployed_dashboard" >/dev/null printf '%s\n' 'deployed_structure_and_theme=passed' ``` -Expected: the deployed object has exactly four datasets, eight layout widgets, six exact business widgets, exact versions and bindings, bare metric-view references, no filters, no Genie link, and the complete light and dark theme. +Expected: the deployed serialization exactly equals the source contract. Execute the deployed queries through the same API assertions: From 6423efa8c9073ce3fdc4a92f1c5fc1e41cc9a641 Mon Sep 17 00:00:00 2001 From: ivancalvo-dbxs Date: Wed, 2 Sep 2026 13:05:14 -0600 Subject: [PATCH 13/13] fix: normalize the deployed dashboard namespace --- .../content/02-databricks-projects/dashboards.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md index acbc52b..429a5f8 100644 --- a/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md +++ b/docs/agentic-starter-journey/content/02-databricks-projects/dashboards.md @@ -460,11 +460,16 @@ trap 'rm -f "$deployed_dashboard"' EXIT jq -er '.serialized_dashboard | fromjson' <<<"$draft" >"$deployed_dashboard" -jq -e --slurpfile source src/bakehouse_franchise_performance.lvdash.json '. == $source[0]' "$deployed_dashboard" >/dev/null +jq -e \ + --arg catalog "$catalog" \ + --slurpfile source src/bakehouse_franchise_performance.lvdash.json ' + all(.datasets[]; .catalog == $catalog and .schema == "bakehouse_gold") + and ((.datasets |= map(del(.catalog, .schema))) == $source[0])' \ + "$deployed_dashboard" >/dev/null printf '%s\n' 'deployed_structure_and_theme=passed' ``` -Expected: the deployed serialization exactly equals the source contract. +Expected: every deployed dataset has the active catalog and `bakehouse_gold` schema, and removing only those platform-injected fields makes the deployed serialization exactly equal the source contract. Execute the deployed queries through the same API assertions: @@ -498,7 +503,7 @@ No visual inspection may substitute for these executable checks. | Duplicate-dashboard assertion fails | More than one dashboard ends with the configured name or the only match has another ID | Use bundle summary to identify the bundle-owned dashboard, reconcile only dashboards whose ownership is proven, leave unproven suffix matches unchanged, and require the sole suffix match to equal the bundle-summary ID | | Publish, draft GET, or published GET returns another display name | Lakeview state is stale or the response was compared with the configured name instead of the effective name | Refresh bundle summary, derive `effective_display_name` again, and require publish, get, and get-published to return that exact value before continuing | | Publish fails or the published check fails | The principal cannot publish, the warehouse is wrong, or no published revision exists | Correct permission or warehouse access, republish the positional ID, and repeat both checks | -| Deployed structure assertion fails | Server serialization or the deployed source differs from the locked contract | Compare the extracted object with the source, correct the source, and redeploy | +| Deployed structure assertion fails | A deployed dataset has the wrong namespace or another deployed field differs from the locked source contract | Compare the extracted object with the source, correct the source or resource namespace, and redeploy | | Deployed dataset assertion fails | Deployed SQL or source values differ from the verified local contract | Stop and reconcile the extracted queries and metric-view data | | Bundle deployment reports drift after UI edits | The workspace draft was changed outside the bundle | Treat the committed source as authoritative and redeploy it through the bundle |