{"slug": "build-an-interactive-operations-dashboard-with-nocobase", "title": "Build an Interactive Operations Dashboard with NocoBase", "summary": "NocoBase, an AI-powered no-code/low-code development platform, published a blog post demonstrating how to build an interactive operations dashboard for a ticketing system by combining its native blocks (filter, chart) with custom JavaScript blocks. The approach balances configuration ease with personalized styling and complex interactions, applicable to CRM, project management, and other business systems.", "body_md": "This article uses an operations dashboard for a ticketing system as an example. It shows how to combine NocoBase chart blocks, filter blocks, and JS blocks to build a data dashboard that supports linked filters, chart drilldowns, and custom styling.\n\nAlthough the example comes from a ticketing scenario, the same approach also applies to CRM, equipment maintenance, project management, approval workflows, customer success, and other business systems.\n\n💡 The point of this article is not to show how to write a large dashboard with a JS block. Instead, it explains how to combine NocoBase native blocks with JS blocks: native blocks handle standard capabilities, while JS blocks fill in the personalized experience.\n\n💬 Hey, you’re reading the NocoBase blog. NocoBase is the most extensible AI-powered no-code/low-code development platform for building enterprise applications, internal tools, and all kinds of systems. It’s fully self-hosted, plugin-based, and developer-friendly. → [Explore NocoBase on GitHub](https://github.com/nocobase/nocobase)\n\n## Scenario Goal\n\nWe want to build an Operations dashboard that helps operations or service teams quickly understand the current workload:\n\n- How many tickets are still open\n- Which tickets have SLA risks\n- How new ticket volume is trending\n- How tickets are distributed by status and priority\n- How to view matching records after clicking a chart\n\nThe page can be roughly divided into four layers:\n\n- Top filter area: time, service group, request type, priority, and SLA status\n- KPI area: Open backlog, Unassigned, SLA warning, and more\n- Chart analysis area: trends, status distribution, SLA distribution, and priority mix\n- Drilldown detail area: show matching records after a chart click\n\n## Start with the Right Building Approach\n\nWhen building a data dashboard, it is easy to turn the problem into a binary choice.\n\nEither you use only NocoBase native blocks, which are easy to configure but may feel less flexible in styling and interaction; or you write a large JS block and control queries, charts, filters, and drilldowns yourself, which means losing much of the convenience that low-code configuration provides.\n\nA better approach is to combine the two.\n\nIn this Operations dashboard, we do not write the entire page as one large JS dashboard. Instead, we split responsibilities:\n\n- The top filters use NocoBase’s built-in filter block.\n- Trend charts, status distribution, and SLA distribution use native chart blocks.\n- KPI cards and drilldown details use JS blocks.\n- The filter block affects both chart blocks and JS blocks.\n- After a chart click, the drilldown condition is passed to the JS detail block below.\n\nThe benefit is clear: standard statistics and filtering still keep NocoBase’s configuration capabilities, while personalized display and complex interactions are handled by JS blocks. The page is neither “configuration only” nor “all code.” Configuration and code each handle what they are good at.\n\n## 1. How to Customize Chart Block Styles\n\nIn a NocoBase chart block, you can first define the aggregation logic with Query builder, and then adjust the style with a custom ECharts option.\n\nUsing “ticket status statistics” as an example, Query builder can be configured as follows:\n\n- Data table: tickets\n- Metric: id count, alias ticketCount\n- Dimension: status\n\nThe key point is that when customizing the style, you do not need to rewrite the query. You only need to process the chart display based on `ctx.data.objects`\n\n.\n\n``` js\nconst rows = Array.isArray(ctx.data?.objects) ? ctx.data.objects : [];\n```\n\nThis line reads the chart query result. Then define the status labels and colors:\n\n``` js\nconst labels = {\n  new: ctx.t('New'),\n  open: ctx.t('Open'),\n  pending_customer: ctx.t('Pending customer'),\n  resolved: ctx.t('Resolved'),\n  closed: ctx.t('Closed'),\n};\n\nconst colors = {\n  new: '#1677ff',\n  open: '#22a06b',\n  pending_customer: '#f59f00',\n  resolved: '#13c2c2',\n  closed: '#8c8c8c',\n};\n```\n\nIt is recommended to use `ctx.t()`\n\nfor all visible text so that multilingual support can be added later.\n\nWhen generating chart data, you can attach drilldown information to each chart data point:\n\n``` js\nconst data = rows.map((row) => ({\n  value: Number(row.ticketCount || 0),\n  itemStyle: {\n    color: colors[row.status] || '#8c8c8c',\n    borderRadius: [6, 6, 0, 0],\n  },\n  ticketingDrilldown: {\n    label: ctx.t('Status') + ': ' + (labels[row.status] || row.status),\n    filter: { status: { $eq: row.status } },\n  },\n}));\n```\n\nThe most important part here is `ticketingDrilldown`\n\n. It is not a standard ECharts field. It is business context that we put into the data ourselves, and it will be used later when the user clicks the chart.\n\nFinally, return the ECharts option:\n\n```\nreturn {\n  grid: { top: 28, right: 22, bottom: 48, left: 42 },\n  tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },\n  xAxis: {\n    type: 'category',\n    data: rows.map((row) => labels[row.status] || row.status),\n  },\n  yAxis: {\n    type: 'value',\n    minInterval: 1,\n  },\n  series: [\n    {\n      name: ctx.t('Tickets'),\n      type: 'bar',\n      barWidth: 36,\n      data,\n    },\n  ],\n};\n```\n\nThe core idea of this part is:\n\n- Query builder is responsible for statistical data.\n- Custom option is responsible for visual expression.\n- Custom fields carry drilldown context.\n\n## 2. Make the System Filter Block the Observation Scope for the Whole Page\n\nThe filter area in an operations dashboard should not be an isolated form. It represents the current observation scope of the entire page.\n\nFor example, when a user selects a service group, a request type, and a creation time range, the KPI cards, trend chart, status distribution, and drilldown details should all be displayed under the same conditions. Otherwise, numbers from different blocks may conflict with each other, making it hard for users to know which result belongs to the current scope.\n\nHere, we directly use NocoBase’s built-in filter block instead of writing a custom filter component. The native filter block can be naturally bound to chart blocks, allowing the Chart block to continue using Query builder, permissions, refresh, and filtering mechanisms.\n\nThe top `Dashboard scope`\n\ncan include these filter fields:\n\n- Created at\n- Service group\n- Request type\n- Priority\n- SLA status\n\nFor JS blocks, you only need to read the same set of filter conditions in code and convert them into a query filter. This allows KPI cards and drilldown details to stay consistent with native charts.\n\nThe filter combination can be wrapped as a small function:\n\n``` js\nfunction combineFilters(...filters) {\n  const parts = filters.filter(Boolean);\n  if (!parts.length) return undefined;\n  if (parts.length === 1) return parts[0];\n  return { $and: parts };\n}\n```\n\nCount records by filter condition:\n\n``` js\nasync function countTickets(filter) {\n  const resource = ctx.makeResource('MultiRecordResource');\n  resource.setResourceName('tickets');\n  resource.setPageSize(1);\n\n  if (filter) {\n    resource.setFilter(filter);\n  }\n\n  await resource.refresh();\n\n  const meta = resource.getMeta?.() || {};\n  return Number(meta.count || meta.total || 0);\n}\n```\n\nThe key lines are:\n\n```\nresource.setFilter(filter);\nawait resource.refresh();\n```\n\nJS blocks query business data through resource instead of directly writing SQL. This makes it easier to stay aligned with NocoBase permissions, data sources, and page runtime behavior.\n\n## 3. Use JS Blocks to Display KPI Cards\n\nKPI cards are better suited to JS blocks, because a KPI is usually not a single query. It is often a combination of several business metrics, such as unfinished tickets, unassigned tickets, SLA warning, SLA breached, new tickets, and resolved tickets.\n\nA JS block can re-query data according to the current filter scope and render the results as statistic cards.\n\n``` js\nconst { Card, Col, Row, Statistic, Tag } = ctx.libs.antd;\n\nconst scopeFilter = getDashboardScopeFilter();\n\nconst openBacklog = await countTickets(\n  combineFilters(scopeFilter, {\n    status: { $notIn: ['resolved', 'closed', 'cancelled'] },\n  }),\n);\n\nctx.render(\n  <Row gutter={[12, 12]}>\n    <Col span={6}>\n      <Card size=\"small\">\n        <Tag color=\"blue\">{ctx.t('Active')}</Tag>\n        <Statistic title={ctx.t('Open backlog')} value={openBacklog} />\n      </Card>\n    </Col>\n  </Row>,\n);\n```\n\nThe key points for JS blocks are:\n\n- Use\n`ctx.makeResource()`\n\nto query data. - Use\n`ctx.libs.antd`\n\nto render the UI. - Use\n`ctx.render()`\n\nto output content. - Re-render the JS block after filters change.\n\nOn a real page, filter and reset buttons can be configured with event flows. After they complete the native filter action, they can also refresh the KPI JS block and the drilldown JS block. This way, one click updates both the charts and the custom content under the same scope.\n\n## 4. Link Chart Blocks with a JS Block for Drilldown\n\nChart drilldown is a very useful dashboard interaction.\n\nIn a ticketing scenario, when the user clicks the “Status: Open” bar, the detail area below shows all Open tickets. When the user clicks “SLA breached,” the detail area shows all overdue tickets.\n\nThe implementation idea is:\n\n- Attach\n`ticketingDrilldown`\n\nto chart data points. - Read the drilldown information from the chart event.\n- Write the drilldown information into the target JS block context.\n- Trigger the target JS block to re-render.\n\nThe key code in the chart event is shown below. First, locate the drilldown JS block:\n\n``` js\nconst DRILLDOWN_TARGET_UID = 'v7mioopm6rm';\n\nfunction getDrilldownTarget() {\n  if (typeof ctx.getModel === 'function') {\n    return ctx.getModel(DRILLDOWN_TARGET_UID);\n  }\n\n  const engine = ctx.model?.flowEngine || ctx.model?.context?.flowEngine || ctx.engine;\n  return engine?.getModel?.(DRILLDOWN_TARGET_UID);\n}\n```\n\nThen write the drilldown condition from the chart click into the target block:\n\n```\nfunction applyDrilldown(drilldown) {\n  if (!drilldown?.filter) return;\n\n  const target = getDrilldownTarget();\n  if (!target?.context?.defineProperty) return;\n\n  target.context.defineProperty('ticketingDashboardDrilldown', {\n    value: drilldown,\n  });\n\n  target.rerender?.();\n}\n```\n\nThe most important lines are:\n\n```\ntarget.context.defineProperty('ticketingDashboardDrilldown', { value: drilldown });\ntarget.rerender?.();\n```\n\nThe first line passes the drilldown condition to the JS block, and the second line triggers the JS block to refresh.\n\nFinally, bind the chart click event:\n\n``` js\nconst clickHandler = (params) => {\n  applyDrilldown(params?.data?.ticketingDrilldown);\n};\n\nchart.on('click', clickHandler);\n\nreturn () => chart.off('click', clickHandler);\n```\n\nIt is strongly recommended to return a cleanup function:\n\n``` js\nreturn () => chart.off('click', clickHandler);\n```\n\nThis cleans up old events when the chart is reconfigured or re-rendered, avoiding duplicate event bindings.\n\nThe chart click event code above applies to [v2.2.0-beta.10](https://github.com/nocobase/nocobase/releases/tag/v2.2.0-beta.10) and later. For earlier versions, refer to:\n\n```\nchart.off('click');\nchart.on('click', clickHandler);\n```\n\n## 5. How the Drilldown JS Block Displays Details\n\nThe drilldown JS block reads the `ticketingDashboardDrilldown`\n\nwritten earlier, and then queries data based on its filter.\n\n``` js\nconst drilldown = ctx.model?.context?.ticketingDashboardDrilldown;\n\nif (!drilldown) {\n  ctx.render(\n    <Alert\n      type=\"info\"\n      showIcon\n      message={ctx.t('Select a chart segment to inspect matching tickets')}\n    />,\n  );\n  return;\n}\n```\n\nIf the user has not clicked a chart yet, a prompt is displayed. After a click, tickets are queried according to `drilldown.filter`\n\n:\n\n``` js\nconst resource = ctx.makeResource('MultiRecordResource');\nresource.setResourceName('tickets');\nresource.setFilter(drilldown.filter);\nresource.setPageSize(10);\nawait resource.refresh();\n\nconst rows = resource.getData?.() || [];\n```\n\nThen render the table:\n\n```\nconst { Table, Typography } = ctx.libs.antd;\n\nctx.render(\n  <>\n    <Typography.Title level={5}>\n      {ctx.t('Drilldown')}: {drilldown.label}\n    </Typography.Title>\n\n    <Table\n      size=\"small\"\n      rowKey=\"id\"\n      dataSource={rows}\n      pagination={false}\n      columns={[\n        { title: ctx.t('Ticket No'), dataIndex: 'ticketNo' },\n        { title: ctx.t('Title'), dataIndex: 'title' },\n        { title: ctx.t('Status'), dataIndex: 'status' },\n        { title: ctx.t('Priority'), dataIndex: 'priority' },\n      ]}\n    />\n  </>,\n);\n```\n\nTo clear the drilldown condition, you can refer to:\n\n```\nfunction clearChartDrilldown() {\n  if (ctx.model?.context?.defineProperty) {\n    ctx.model.context.defineProperty('ticketingDashboardDrilldown', { value: null });\n  }\n  if (typeof ctx.model?.rerender === 'function') {\n    ctx.model.rerender();\n  }\n}\n```\n\nThe key points here are:\n\n- The chart only passes the filter.\n- The JS block is responsible for querying and displaying details.\n- The same drilldown block can be reused by clicks from different charts.\n\n## Practical Recommendations\n\n### 1. Do Not Rush to Code the Whole Complex Page\n\nThe most important lesson from this page is: do not treat native capabilities and JS capabilities as opposites.\n\nIf a capability is already native to NocoBase, such as filtering, chart queries, table display, permission control, use the native block first. This allows you to continue configuring fields, filter conditions, and chart metrics in the interface later.\n\nJS blocks are better for parts that native blocks are not good at, such as combining multiple metrics into KPI cards, applying special card styles, showing custom details after a chart click, or passing business context between different blocks.\n\nIn other words, native blocks handle “configurable standard capabilities,” while JS blocks handle “business-specific personalized experiences.” This is the most reusable building approach from this dashboard.\n\n### 2. Use Chart Block Query Builder First for Simple Statistics\n\nThis preserves NocoBase’s standard query, permission, filtering, and refresh capabilities. Only use a custom ECharts option for visual optimization when the default chart style cannot express the business emphasis clearly.\n\n### 3. Use JS Blocks First for KPI Cards\n\nKPIs usually need multiple queries, condition combinations, and custom layouts, so JS blocks are more flexible. This is especially clear when KPIs need to respond to the same set of system filter conditions.\n\n### 4. Return Cleanup for Chart Events\n\nRecommended pattern:\n\n``` js\nconst handler = (params) => {\n  // handle click\n};\n\nchart.on('click', handler);\n\nreturn () => chart.off('click', handler);\n```\n\nDo not directly use `chart.off('click')`\n\nto clear all click events, because this may accidentally remove listeners used by the chart block or configuration panel.\n\n## Let AI Help You Build It\n\nThis type of dashboard is very suitable for AI-assisted generation, because it involves data models, statistical definitions, chart styling, and page interactions at the same time. You can give this article to an AI tool and ask questions based on the prompt below.\n\nYou can ask:\n\n```\nI am building an operations dashboard for a ticketing system with NocoBase.\nPlease use the ticketing scenario as an example and help me design an Operations dashboard.\n\nThe tickets table contains:\nticketNo, title, status, priority, slaStatus,\nrequestType, serviceGroup, assignee, createdAt, updatedAt.\n\nThe page needs:\n1. Top filters: Created at, Service group, Request type, Priority, SLA status.\n2. KPI cards: Open backlog, Unassigned, SLA warning, SLA breached, New tickets, Resolved tickets.\n3. Charts: Created tickets trend, Ticket status, SLA status, Priority mix.\n4. After a chart click, the JS block below displays a matching Ticket drilldown table.\n5. The chart style should fit an operations dashboard, with clear colors and a compact layout.\n6. All JS text should use ctx.t().\n7. Chart events should use chart.on and return a cleanup function.\n8. Prefer NocoBase native filter blocks and chart blocks. Use JS blocks only for KPIs, drilldown details, special styling, and cross-block interactions. Do not write the whole page as one large JS block.\n\nPlease provide the configuration approach for each block and mark the key JS code.\n```\n\nIf you already have a page, you can also ask AI to optimize it:\n\n```\nThis is my current NocoBase dashboard design:\nThe top area is a filter block, the middle area has four charts, and the bottom area has a drilldown JS block.\nPlease help me optimize it from the perspective of operations staff:\n1. Which KPIs should be displayed?\n2. Do the charts need to be linked?\n3. Which columns should be shown in drilldown details?\n4. How should JS blocks and chart events be organized?\n5. Which code should be placed in the chart custom option, and which code should be placed in the JS block?\n```\n\nThis helps the AI generate content that is closer to real business scenarios instead of isolated code snippets.\n\nIf you choose to let AI help you build, back up the project with Backup Manager before you start.", "url": "https://wpnews.pro/news/build-an-interactive-operations-dashboard-with-nocobase", "canonical_source": "https://www.nocobase.com/en/blog/build-interactive-operations-dashboard-with-nocobase/", "published_at": "2026-07-09 05:27:00+00:00", "updated_at": "2026-07-09 08:44:42.433362+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["NocoBase"], "alternates": {"html": "https://wpnews.pro/news/build-an-interactive-operations-dashboard-with-nocobase", "markdown": "https://wpnews.pro/news/build-an-interactive-operations-dashboard-with-nocobase.md", "text": "https://wpnews.pro/news/build-an-interactive-operations-dashboard-with-nocobase.txt", "jsonld": "https://wpnews.pro/news/build-an-interactive-operations-dashboard-with-nocobase.jsonld"}}