Create Dashboard with Kopai
Build a dashboard by writing a short TypeScript script: design a uiTree, feed each
data tile with a type-safe kq query, and create it via client.createDashboard(...).
The CLI stays as a fallback for one-off creates.
Component Schema (auto-generated)
!npx @kopai/cli dashboards schema 2>/dev/null || echo "ERROR: Cannot connect to Kopai backend. If running locally, start it with: npx @kopai/app start — If using a remote backend, check the url in your .kopairc file."
Available Metrics
!npx @kopai/cli metrics discover --json 2>/dev/null || echo "ERROR: Cannot connect to Kopai backend. If running locally, start it with: npx @kopai/app start — If using a remote backend, check the url in your .kopairc file."
Code mode (recommended)
Connect — clientFromConfig() reads .kopairc just like the CLI:
import { clientFromConfig } from "@kopai/sdk/node";
const client = clientFromConfig(); // ./.kopairc → ~/.kopairc → http://localhost:8000
Run — npx tsx dashboard.mts. Use .mts so top-level await works regardless
of the project's module type. Requires @kopai/sdk installed (npm i @kopai/sdk).
Output — console.log(dash.id) so the new dashboard id lands on stdout. Wrap the
build + create in try/catch.
Typed results — discoverMetrics() and client.query(q) return fully typed values
(metric name/type/unit, aggregate measure values as number, etc.). When you
probe/verify data, iterate the results directly — don't cast to any/any[].
Two rules that will save you a broken dashboard
- Feed data tiles with a RAW query —
kq.metrics("<Type>").raw()…build(), not .aggregate().
The tile renderers (MetricStat/MetricTimeSeries/MetricHistogram/MetricTable, LogTimeline,
TraceDetail) plot raw rows; an aggregate/summary/timeSeries query is rejected
at render with "this panel displays raw metric rows … use a raw metric query". Choose the
MetricType up front via kq.metrics("Gauge") (auto-pinned), add a MetricName filter and
a time window on metric tiles.
- Set every declared prop — use
null for unused ones. The render-time schema
requires nullable props to be present, not omitted. Omitting them passes
createDashboard but fails at render with "invalid layout: …props.description:
expected string, received undefined". So a Card is { title, description: null, padding: null }, a MetricTimeSeries is { height, showBrush: null, yAxisLabel: null, unit }, etc.
Backend caveats
- Every tile query needs a time window —
.timeRelative("1h") / .timeAbsolute(startISO, endISO).
- Choose the
MetricType up front via kq.metrics("Gauge") — it's auto-pinned, no manual .where("MetricType", …) needed. Use the type from discoverMetrics(). Types: Gauge | Sum | Histogram | ExponentialHistogram | Summary.
- The SDK field is
uiTreeVersion (the CLI flag is --tree-version). Current tree version: 0.14.0.
Workflow
- Discover metrics —
const { metrics } = await client.discoverMetrics(); Each entry
is { name, type, unit, description, attributes, resourceAttributes }. Pass the metric's
type to kq.metrics("<Type>"), and set the tile unit prop from unit.
- Build each tile's query with
kq.metrics("<Type>").raw() (or kq.logs.raw() /
kq.traces.raw()) — returns a KopaiQuery object; hold it in a variable.
- Assemble the
uiTree — embed the built query into the tile:
dataSource: { method: "query", params: <builtQuery> }. Set every prop (null for unused).
- Create —
const dash = await client.createDashboard({
name: "<name>",
uiTreeVersion: "0.14.0",
uiTree,
metadata: {},
});
console.log(dash.id);
On a KopaiValidationError/KopaiError, re-run discoverMetrics() to recheck names
and types, fix the tree, and retry.
- View — open
<baseUrl>/?tab=metrics&dashboardId=<dash.id>.
Quick Example
// create-cpu-dashboard.mts — run: npx tsx create-cpu-dashboard.mts
import { kq } from "@kopai/sdk";
import { clientFromConfig } from "@kopai/sdk/node";
const client = clientFromConfig();
// Tile data: a RAW metric query (tiles render raw rows, not aggregates).
const cpuSeries = kq
.metrics("Gauge") // MetricType chosen up front, auto-pinned
.raw()
.where((f) => f.eq("MetricName", "system.cpu.utilization"))
.timeRelative("1h")
.limit(500)
.build();
const uiTree = {
root: "stack-1",
elements: {
"stack-1": {
key: "stack-1",
type: "Stack",
props: { direction: "vertical", gap: "md", align: null },
children: ["card-1"],
parentKey: "",
},
"card-1": {
key: "card-1",
type: "Card",
props: { title: "CPU Usage", description: null, padding: null },
children: ["ts-1"],
parentKey: "stack-1",
},
"ts-1": {
key: "ts-1",
type: "MetricTimeSeries",
props: { height: 300, showBrush: null, yAxisLabel: null, unit: "1" },
children: [],
parentKey: "card-1",
dataSource: { method: "query", params: cpuSeries },
},
},
};
const dash = await client.createDashboard({
name: "CPU Dashboard",
uiTreeVersion: "0.14.0",
uiTree,
metadata: {},
});
console.log(`Created dashboard ${dash.id}`);
Components
Layout (have children, no dataSource): Stack, Grid, Card. Static:
Heading, Text, Badge, Divider, Empty.
Data tiles — feed each with a raw kq query via method: "query" (legacy methods
still work, listed for fallback):
| Component |
Best for |
Metric types |
dataSource methods |
kq params example (RAW) |
| MetricStat |
KPI number |
Sum, Gauge |
searchMetricsPage, searchAggregatedMetrics, query |
kq.metrics("Sum").raw().where(f=>f.eq("MetricName",NAME)).timeRelative("1h").limit(500).build() |
| MetricTimeSeries |
Trend chart |
Sum, Gauge, Histogram |
searchMetricsPage, query |
kq.metrics("Gauge").raw().where(f=>f.eq("MetricName",NAME)).timeRelative("1h").limit(500).build() |
| MetricHistogram |
Distribution |
Histogram, ExponentialHistogram |
searchMetricsPage, query |
kq.metrics("Histogram").raw().where(f=>f.eq("MetricName",NAME)).timeRelative("1h").limit(500).build() |
| MetricTable |
Tabular |
any |
searchMetricsPage, query |
kq.metrics("Gauge").raw().where(f=>f.eq("MetricName",NAME)).timeRelative("1h").limit(500).build() |
| LogTimeline |
Log stream |
n/a |
searchLogsPage, query |
kq.logs.raw().where(f=>f.gte("SeverityNumber",17)).timeRelative("1h").limit(100).build() |
| TraceDetail |
Trace inspector |
n/a |
searchTracesPage, searchTraceSummariesPage, query |
kq.traces.raw().where(f=>f.eq("StatusCode","Error")).timeRelative("1h").limit(50).build() |
Sizing/props: set height: 300 on MetricTimeSeries/MetricHistogram, height: 600 on
LogTimeline (smaller collapses it to a count badge); MetricStat needs no height. Set
unit on chart tiles to the metric's raw OTEL unit ("By", "s", "ms", "1"). And
remember rule #2: every declared prop must be present (null for unused).
Rules
workflow - Dashboard creation workflow (tree structure, dataSource rules, layout, error handling)
Read rules/<rule-name>.md for details.
CLI fallback (one-off create)
The CLI takes the tree on stdin and uses --tree-version (the SDK field is uiTreeVersion).
It cannot build kq queries, so tiles use the legacy searchMetricsPage/searchLogsPage form:
echo '{"uiTree":{…},"metadata":{}}' | npx @kopai/cli dashboards create --name "<name>" --tree-version "0.14.0" --json
1---2name: create-dashboard3description: Build Kopai observability dashboards from OpenTelemetry metrics, logs, and traces by composing tiles — time-series charts, stat/KPI numbers, histograms, log timelines, and trace-detail views — into a dashboard via the Kopai SDK. Use this skill whenever the user wants to visualize, chart, monitor, or 'keep an eye on' telemetry — e.g. 'make a dashboard', 'monitoring view', 'KPI panel', 'chart request latency and throughput', 'an incident board with error logs and a trace viewer' — even if they don't say the word 'dashboard' — and when another workflow needs to present telemetry visually (e.g. after a root-cause analysis). This creates Kopai dashboards specifically. Do NOT use it to investigate or root-cause an issue (use root-cause-analysis), to add instrumentation (use otel-instrumentation), to build Grafana/Prometheus dashboards, or to code React/UI chart components.4license: Apache-2.05---67# Create Dashboard with Kopai89Build a dashboard by writing a short TypeScript script: design a `uiTree`, feed each10data tile with a type-safe `kq` query, and create it via `client.createDashboard(...)`.11The CLI stays as a fallback for one-off creates.1213## Component Schema (auto-generated)1415!`npx @kopai/cli dashboards schema 2>/dev/null || echo "ERROR: Cannot connect to Kopai backend. If running locally, start it with: npx @kopai/app start — If using a remote backend, check the url in your .kopairc file."`1617## Available Metrics1819!`npx @kopai/cli metrics discover --json 2>/dev/null || echo "ERROR: Cannot connect to Kopai backend. If running locally, start it with: npx @kopai/app start — If using a remote backend, check the url in your .kopairc file."`2021## Code mode (recommended)2223**Connect** — `clientFromConfig()` reads `.kopairc` just like the CLI:2425```ts26import { clientFromConfig } from "@kopai/sdk/node";27const client = clientFromConfig(); // ./.kopairc → ~/.kopairc → http://localhost:800028```2930**Run** — `npx tsx dashboard.mts`. Use **`.mts`** so top-level `await` works regardless31of the project's module type. Requires `@kopai/sdk` installed (`npm i @kopai/sdk`).3233**Output** — `console.log(dash.id)` so the new dashboard id lands on stdout. Wrap the34build + create in try/catch.3536**Typed results** — `discoverMetrics()` and `client.query(q)` return fully typed values37(metric `name`/`type`/`unit`, aggregate measure values as `number`, etc.). When you38probe/verify data, iterate the results directly — **don't cast to `any`/`any[]`**.3940## Two rules that will save you a broken dashboard41421. **Feed data tiles with a RAW query** — `kq.metrics("<Type>").raw()…build()`, not `.aggregate()`.43 The tile renderers (MetricStat/MetricTimeSeries/MetricHistogram/MetricTable, LogTimeline,44 TraceDetail) plot **raw rows**; an aggregate/`summary`/`timeSeries` query is rejected45 at render with _"this panel displays raw metric rows … use a raw metric query"_. Choose the46 `MetricType` up front via `kq.metrics("Gauge")` (auto-pinned), add a `MetricName` filter and47 a time window on metric tiles.482. **Set every declared prop — use `null` for unused ones.** The render-time schema49 requires nullable props to be _present_, not omitted. Omitting them passes50 `createDashboard` but fails at render with _"invalid layout: …props.description:51 expected string, received undefined"_. So a Card is `{ title, description: null,52padding: null }`, a MetricTimeSeries is `{ height, showBrush: null, yAxisLabel: null,53unit }`, etc.5455## Backend caveats5657- **Every tile query needs a time window** — `.timeRelative("1h")` / `.timeAbsolute(startISO, endISO)`.58- **Choose the `MetricType` up front via `kq.metrics("Gauge")`** — it's auto-pinned, no manual `.where("MetricType", …)` needed. Use the type from `discoverMetrics()`. Types: `Gauge | Sum | Histogram | ExponentialHistogram | Summary`.59- The SDK field is **`uiTreeVersion`** (the CLI flag is `--tree-version`). Current tree version: **`0.14.0`**.6061## Workflow62631. **Discover metrics** — `const { metrics } = await client.discoverMetrics();` Each entry64 is `{ name, type, unit, description, attributes, resourceAttributes }`. Pass the metric's65 `type` to `kq.metrics("<Type>")`, and set the tile `unit` prop from `unit`.662. **Build each tile's query with `kq.metrics("<Type>").raw()`** (or `kq.logs.raw()` /67 `kq.traces.raw()`) — returns a `KopaiQuery` object; hold it in a variable.683. **Assemble the `uiTree`** — embed the built query into the tile:69 `dataSource: { method: "query", params: <builtQuery> }`. Set every prop (null for unused).704. **Create** —71 ```ts72 const dash = await client.createDashboard({73 name: "<name>",74 uiTreeVersion: "0.14.0",75 uiTree,76 metadata: {},77 });78 console.log(dash.id);79 ```80 On a `KopaiValidationError`/`KopaiError`, re-run `discoverMetrics()` to recheck names81 and types, fix the tree, and retry.825. **View** — open `<baseUrl>/?tab=metrics&dashboardId=<dash.id>`.8384## Quick Example8586```ts87// create-cpu-dashboard.mts — run: npx tsx create-cpu-dashboard.mts88import { kq } from "@kopai/sdk";89import { clientFromConfig } from "@kopai/sdk/node";90const client = clientFromConfig();9192// Tile data: a RAW metric query (tiles render raw rows, not aggregates).93const cpuSeries = kq94 .metrics("Gauge") // MetricType chosen up front, auto-pinned95 .raw()96 .where((f) => f.eq("MetricName", "system.cpu.utilization"))97 .timeRelative("1h")98 .limit(500)99 .build();100101const uiTree = {102 root: "stack-1",103 elements: {104 "stack-1": {105 key: "stack-1",106 type: "Stack",107 props: { direction: "vertical", gap: "md", align: null },108 children: ["card-1"],109 parentKey: "",110 },111 "card-1": {112 key: "card-1",113 type: "Card",114 props: { title: "CPU Usage", description: null, padding: null },115 children: ["ts-1"],116 parentKey: "stack-1",117 },118 "ts-1": {119 key: "ts-1",120 type: "MetricTimeSeries",121 props: { height: 300, showBrush: null, yAxisLabel: null, unit: "1" },122 children: [],123 parentKey: "card-1",124 dataSource: { method: "query", params: cpuSeries },125 },126 },127};128129const dash = await client.createDashboard({130 name: "CPU Dashboard",131 uiTreeVersion: "0.14.0",132 uiTree,133 metadata: {},134});135console.log(`Created dashboard ${dash.id}`);136```137138## Components139140Layout (have children, no `dataSource`): **Stack**, **Grid**, **Card**. Static:141**Heading**, **Text**, **Badge**, **Divider**, **Empty**.142143Data tiles — feed each with a **raw** `kq` query via `method: "query"` (legacy methods144still work, listed for fallback):145146| Component | Best for | Metric types | dataSource methods | `kq` params example (RAW) |147| ---------------- | --------------- | ------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |148| MetricStat | KPI number | Sum, Gauge | `searchMetricsPage`, `searchAggregatedMetrics`, `query` | `kq.metrics("Sum").raw().where(f=>f.eq("MetricName",NAME)).timeRelative("1h").limit(500).build()` |149| MetricTimeSeries | Trend chart | Sum, Gauge, Histogram | `searchMetricsPage`, `query` | `kq.metrics("Gauge").raw().where(f=>f.eq("MetricName",NAME)).timeRelative("1h").limit(500).build()` |150| MetricHistogram | Distribution | Histogram, ExponentialHistogram | `searchMetricsPage`, `query` | `kq.metrics("Histogram").raw().where(f=>f.eq("MetricName",NAME)).timeRelative("1h").limit(500).build()` |151| MetricTable | Tabular | any | `searchMetricsPage`, `query` | `kq.metrics("Gauge").raw().where(f=>f.eq("MetricName",NAME)).timeRelative("1h").limit(500).build()` |152| LogTimeline | Log stream | n/a | `searchLogsPage`, `query` | `kq.logs.raw().where(f=>f.gte("SeverityNumber",17)).timeRelative("1h").limit(100).build()` |153| TraceDetail | Trace inspector | n/a | `searchTracesPage`, `searchTraceSummariesPage`, `query` | `kq.traces.raw().where(f=>f.eq("StatusCode","Error")).timeRelative("1h").limit(50).build()` |154155Sizing/props: set `height: 300` on MetricTimeSeries/MetricHistogram, `height: 600` on156LogTimeline (smaller collapses it to a count badge); MetricStat needs no height. Set157`unit` on chart tiles to the metric's raw OTEL unit (`"By"`, `"s"`, `"ms"`, `"1"`). And158remember rule #2: every declared prop must be present (null for unused).159160## Rules161162- `workflow` - Dashboard creation workflow (tree structure, dataSource rules, layout, error handling)163164Read `rules/<rule-name>.md` for details.165166## CLI fallback (one-off create)167168The CLI takes the tree on stdin and uses `--tree-version` (the SDK field is `uiTreeVersion`).169It cannot build `kq` queries, so tiles use the legacy `searchMetricsPage`/`searchLogsPage` form:170171```bash172echo '{"uiTree":{…},"metadata":{}}' | npx @kopai/cli dashboards create --name "<name>" --tree-version "0.14.0" --json173```