Skip to content

A KPI dashboard page

When you need this

A page (or the top of a list page) showing several headline numbers computed server-side, fetched in one call, refreshed after writes.

The pieces

One count Data Service per metric

json
{
  "name": "oeq-equipment-count-available",
  "definition": {
    "operation": "count",
    "source": { "kind": "entity", "entityName": "oeq_equipment" },
    "filters": [ { "field": "status", "operator": "eq", "value": "AVAILABLE" } ],
    "parameters": []
  }
}

One composite Data Service wrapping them

metadata/data_service/oeq-equipment-kpis.json (real file):

json
{
  "name": "oeq-equipment-kpis",
  "definition": {
    "operation": "composite",
    "source": { "kind": "entity", "entityName": "oeq_equipment" },
    "steps": [
      { "as": "total",     "service": "oeq-equipment-count-total",     "parameters": {} },
      { "as": "available", "service": "oeq-equipment-count-available", "parameters": {} },
      { "as": "assigned",  "service": "oeq-equipment-count-assigned",  "parameters": {} }
    ]
  }
}

The page mount loader

json
"events": { "mounted": { "source": "action-chain", "actions": [
  { "id": "a0", "order": 0, "type": "callApi",
    "config": { "connectionRef": "self", "path": "/api/v1/data-services/oeq-equipment-kpis/execute", "httpMethod": "POST", "params": {} }, "output": "kpi" },
  { "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.kpiTotal",     "value": "${kpi.results.total.value}" } },
  { "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.kpiAvailable", "value": "${kpi.results.available.value}" } },
  { "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.kpiAssigned",  "value": "${kpi.results.assigned.value}" } }
] } }

The cards

json
{
  "blockType": "core.kpi-card",
  "properties": {
    "value": { "source": "binding", "binding": { "scope": "page", "key": "kpiTotal" } },
    "labelKey": { "source": "static", "value": "office-equipment.equipment-register.kpi.total" },
    "format": { "source": "static", "value": "number" },
    "colorToken": { "source": "static", "value": "primary" }
  }
}

The rule that bites

count${out.value}. composite${out.results.<step>.value}. Never .total (that's the unrelated list-pagination envelope).

Real response from the tutorial:

json
{ "results": { "total": { "value": 0 }, "available": { "value": 0 }, "assigned": { "value": 0 } } }

To refresh after a write, append the same callApi + setValue steps to the Save action chain.