Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions .github/scripts/verify-dashboards-api-example.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,30 +79,28 @@


def extract_payload(markdown_path: Path) -> dict:
"""Return the parsed JSON payload from the curl example in the markdown."""
"""Return the parsed JSON payload from the Console tab example in the markdown."""
text = markdown_path.read_text(encoding="utf-8")
match = re.search(
r"curl[^\n]*\n(?:[^\n]*\n)*?\s*-d '(\{.*?\})'\n```",
r"```console\nPOST kbn://api/dashboards\n(\{.*?\})\n```",
text,
re.DOTALL,
)
if not match:
# Failure mode 2: the regex couldn't find a curl block matching our
# Failure mode 2: the regex couldn't find a Console block matching our
# expected shape. The page has been restructured, the dropdown was
# removed, or the fence/indent changed. Inspect the markdown to
# confirm the example still exists.
raise SystemExit(
f"Could not find a curl POST example in {markdown_path}. "
f"Could not find a Console POST example in {markdown_path}. "
"Has the page structure changed?"
)
raw = match.group(1)
cleaned = re.sub(r"\s*<\d+>", "", raw)
try:
return json.loads(cleaned)
except json.JSONDecodeError as exc:
# Failure mode 3: a docs edit produced syntactically invalid JSON
# inside the curl block. The parser reports the line/column to look
# at; also worth checking for smart quotes or trailing commas.
# Failure mode 3: a docs edit produced syntactically invalid JSON.
raise SystemExit(f"Extracted JSON is not valid: {exc}") from exc


Expand Down
314 changes: 311 additions & 3 deletions explore-analyze/kibana-data-exploration-learning-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,10 +407,314 @@ Everything you built in this tutorial can also be reproduced in a single API cal
whenever the spec at https://github.com/elastic/dashboards-api-spec is
updated. Last verified against dashboards-api-spec @ 84120e3 on 2026-05-06.
-->
::::{dropdown} Recreate this dashboard with one API call
:::::::{dropdown} Recreate this dashboard with one API call

The following `curl` request creates the same dashboard as the one you built in this tutorial, including the optional panels suggestions.
The following request creates the same dashboard as the one you built in this tutorial, including the optional panels suggestions.

:::::{tab-set}

::::{tab-item} Console
:sync: api-console
```console
POST kbn://api/dashboards
{
"title": "Web logs overview",
"description": "Recreates the dashboard built in the Kibana data exploration tutorial.",
"time_range": { "from": "now-90d", "to": "now" },
"panels": [
{
"grid": { "x": 0, "y": 0, "w": 12, "h": 5 },
"type": "vis",
"config": {
"type": "metric",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_logs",
"time_field": "timestamp"
},
"metrics": [
{
"type": "primary",
"operation": "median",
"field": "bytes",
"label": "Median response size", <1>
"format": { "type": "bytes", "decimals": 2 },
"background_chart": { "type": "trend" },
"color": {
"type": "dynamic",
"range": "absolute",
"steps": [
{ "lt": 6000, "color": "#24c292" },
{ "gte": 6000, "lt": 10000, "color": "#fcd883" },
{ "gte": 10000, "color": "#f6726a" }
]
},
"apply_color_to": "background"
}
]
}
},
{
"grid": { "x": 12, "y": 0, "w": 12, "h": 5 },
"type": "vis",
"config": {
"type": "metric",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_logs",
"time_field": "timestamp"
},
"metrics": [
{
"type": "primary",
"operation": "unique_count",
"field": "clientip",
"label": "Unique visitors" <2>
}
]
}
},
{
"grid": { "x": 24, "y": 0, "w": 12, "h": 5 },
"type": "vis",
"config": {
"type": "metric",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_logs",
"time_field": "timestamp"
},
"metrics": [
{
"type": "primary",
"operation": "count",
"label": "Total requests" <3>
},
{
"type": "secondary",
"operation": "count",
"label": "Week to week",
"time_shift": "1w",
"compare": { "to": "primary", "palette": "compare_to", "icon": true, "value": true }
}
]
}
},
{
"grid": { "x": 36, "y": 0, "w": 12, "h": 5 },
"type": "vis",
"config": {
"type": "metric",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_logs",
"time_field": "timestamp"
},
"metrics": [
{
"type": "primary",
"operation": "unique_count",
"field": "request.keyword",
"label": "Unique URLs" <4>
}
]
}
},
{
"grid": { "x": 0, "y": 5, "w": 24, "h": 10 },
"type": "vis",
"config": {
"type": "xy",
"title": "Response size over time, per host", <5>
"layers": [
{
"type": "line",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_logs",
"time_field": "timestamp"
},
"x": { "operation": "date_histogram", "field": "timestamp" },
"y": [{ "operation": "median", "field": "bytes" }],
"breakdown_by": {
"operation": "terms",
"fields": ["host.keyword"],
"limit": 9
}
}
]
}
},
{
"grid": { "x": 24, "y": 5, "w": 24, "h": 10 },
"type": "vis",
"config": {
"type": "xy",
"title": "Log volume over time, per host", <6>
"layers": [
{
"type": "line",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_logs",
"time_field": "timestamp"
},
"x": { "operation": "date_histogram", "field": "timestamp" },
"y": [{ "operation": "count" }],
"breakdown_by": {
"operation": "terms",
"fields": ["host.keyword"],
"limit": 9
}
},
{
"type": "reference_lines",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_logs",
"time_field": "timestamp"
},
"thresholds": [
{
"operation": "static_value",
"value": 80,
"label": "High traffic",
"color": { "type": "static", "color": "#f6726a" },
"icon": "alert"
}
]
}
]
}
},
{
"grid": { "x": 0, "y": 15, "w": 24, "h": 10 },
"type": "vis",
"config": {
"type": "xy",
"title": "Events by response code", <7>
"layers": [
{
"type": "bar_stacked",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | WHERE response IS NOT NULL | STATS event_count = COUNT(*) BY response | SORT event_count DESC | LIMIT 50"
},
"x": { "column": "response" },
"y": [{ "column": "event_count" }]
}
]
}
},
{
"grid": { "x": 24, "y": 15, "w": 24, "h": 10 },
"type": "vis",
"config": {
"type": "xy",
"title": "Requests by file extension", <8>
"layers": [
{
"type": "bar_stacked",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_logs",
"time_field": "timestamp"
},
"x": {
"operation": "terms",
"fields": ["extension.keyword"],
"limit": 9,
"includes": { "as_regex": true, "values": [".+"] }
},
"y": [{ "operation": "count" }]
}
]
}
},
{
"grid": { "x": 0, "y": 25, "w": 24, "h": 10 },
"type": "vis",
"config": {
"type": "pie",
"title": "Traffic distribution by operating system", <9>
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_logs",
"time_field": "timestamp"
},
"metrics": [{ "operation": "count" }],
"group_by": [
{
"operation": "terms",
"fields": ["machine.os.keyword"],
"limit": 9
}
]
}
},
{
"grid": { "x": 24, "y": 25, "w": 24, "h": 10 },
"type": "vis",
"config": {
"type": "treemap",
"title": "Requests by geography", <10>
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_logs",
"time_field": "timestamp"
},
"metrics": [{ "operation": "count" }],
"group_by": [
{
"operation": "terms",
"fields": ["geo.dest"],
"limit": 9
}
]
}
},
{
"grid": { "x": 0, "y": 35, "w": 48, "h": 17 },
"type": "vis",
"config": {
"type": "data_table",
"title": "Last 100 events", <11>
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | KEEP @timestamp, request, response, bytes | SORT @timestamp DESC | LIMIT 100"
},
"rows": [
{ "column": "@timestamp" },
{ "column": "request" },
{ "column": "response" }
],
"metrics": [
{ "column": "bytes" }
]
}
}
]
}
```

How each panel maps back to the tutorial:

1. **Median response size**: Lens metric panel from the [Add a metric panel for median response size](#add-a-metric-panel-for-median-response-size) sub-step, including the bytes `format`, the trend `background_chart` for the sparkline, and the dynamic `color` thresholds.
2. **Unique visitors**: `unique_count` of `clientip`, from the [Optional: add more metrics to build a row](#add-a-metric-panel-for-median-response-size) suggestion.
3. **Total requests**: `count` with a secondary metric configured for week-over-week comparison via `time_shift: "1w"` and `compare`. Same source as the unique-visitors suggestion.
4. **Unique URLs**: `unique_count` of `request.keyword`, also from the optional metrics row.
5. **Response size over time, per host**: line chart of median `bytes` split by host, from the [Optional: add more time series](#add-a-line-chart-of-log-volume-over-time) suggestion in the line-chart sub-step.
6. **Log volume over time, per host**: the line chart from the [Add a line chart of log volume over time](#add-a-line-chart-of-log-volume-over-time) sub-step. The reference line at value `80` is a separate `reference_lines` layer in the same panel.
7. **Events by response code**: the {{esql}} bar chart saved to the dashboard from Discover in [Step 2](#explore-data-in-discover). ES|QL chart layers reference query result columns directly in `x` and `y` (for example, `"x": { "column": "response" }`), instead of the `operation`-based form used by data view layers.
8. **Requests by file extension**: bar chart from the [Add a bar chart of requests by file extension](#add-a-bar-chart-of-requests-by-file-extension) sub-step. The `includes` filter with `as_regex: true` and value `.+` mirrors the regex applied during the inline-edit step.
9. **Traffic distribution by operating system**: pie chart from the [Expand your dashboard](#expand-your-dashboard) sub-step. Pie panels use `config.type: "pie"` with a `metrics` array and a `group_by` array.
10. **Requests by geography**: treemap from the same sub-step. Treemaps use the same `metrics` + `group_by` shape as pies.
11. **Last 100 events**: {{esql}} data table from the [Add a table of recent events with {{esql}}](#add-a-table-of-recent-events-with-esql) sub-step. Categorical columns go in `rows`, numeric columns go in `metrics`.

::::

::::{tab-item} curl
:sync: api-curl
```bash
curl -X POST "${KIBANA_URL}/api/dashboards" \
-H "Authorization: ApiKey ${API_KEY}" \
Expand Down Expand Up @@ -710,9 +1014,13 @@ How each panel maps back to the tutorial:
10. **Requests by geography**: treemap from the same sub-step. Treemaps use the same `metrics` + `group_by` shape as pies.
11. **Last 100 events**: {{esql}} data table from the [Add a table of recent events with {{esql}}](#add-a-table-of-recent-events-with-esql) sub-step. Categorical columns go in `rows`, numeric columns go in `metrics`.

For the full request schema, including sections, filter controls, and library-linked panels, refer to the [Dashboards API reference](https://elastic.github.io/dashboards-api-spec/dashboards#tag/Dashboards/operation/post-dashboards).
::::

:::::

For the full request schema, including sections, filter controls, and library-linked panels, refer to the [Dashboards API reference](https://elastic.github.io/dashboards-api-spec/dashboards#tag/Dashboards/operation/post-dashboards).
:::::::


## Navigate between Discover and dashboards [navigate-between-apps]

Expand Down
Loading