In this post, we build a real-time monitoring dashboard using Next.js, a React framework that supports server-side rendering, static site generation, and full-stack capabilities with built-in performance optimizations. Similar to the Streamlit app we developed in Part 2, this dashboard connects to the WebSocket server from Part 1 to continuously fetch and visualize key metrics such as order counts, sales data, and revenue by traffic source and country. With interactive bar charts and dynamic metrics, users can monitor sales trends and other critical business KPIs in real-time.
Next.js Frontend
The Next.js dashboard processes and displays real-time theLook eCommerce data. It connects to the WebSocket server using the React useWebSocket package, while the UI is styled with HeroUI (formerly NextUI) and Tailwind CSS. Visualizations are powered by Apache ECharts. The source code for this post is available in this GitHub repository.
Metric Component
We use a React component called Metric
that displays a metric card with the following props:
label
: The title or name of the metric.value
: The value of the metric (could represent a number or currency).delta
: The change in the metric value (used to indicate increase or decrease).is_currency
: A boolean flag to indicate whether the value should be formatted as a currency.
The card’s visual layout includes the label at the top, the formatted value in large text, and the delta change with an arrow beneath it.
1// nextjs/src/components/metric.tsx
2"use client";
3
4import {
5 Card,
6 CardHeader,
7 CardBody,
8 Divider,
9 CardFooter,
10} from "@nextui-org/react";
11
12export interface MetricProps {
13 label: string;
14 value: number;
15 delta: number;
16 is_currency: boolean;
17}
18
19export default function Metric({
20 label,
21 value,
22 delta,
23 is_currency,
24}: MetricProps) {
25 const formatted_value = is_currency
26 ? "$ ".concat(value.toLocaleString())
27 : value.toLocaleString();
28 const arrowColor = delta == 0 ? "black" : delta > 0 ? "green" : "red";
29 return (
30 <div className="col-span-12 md:col-span-4">
31 <Card>
32 <CardHeader>{label}</CardHeader>
33 <CardBody>
34 <h1 className="text-4xl font-bold">{formatted_value}</h1>
35 </CardBody>
36 <Divider />
37 <CardFooter>
38 <svg
39 height={25}
40 viewBox="0 0 24 24"
41 aria-hidden="true"
42 focusable="false"
43 fill={arrowColor}
44 xmlns="http://www.w3.org/2000/svg"
45 color="inherit"
46 data-testid="stMetricDeltaIcon-Up"
47 className="e14lo1l1 st-emotion-cache-1ksdj5j ex0cdmw0"
48 >
49 <path fill="none" d="M0 0h24v24H0V0z"></path>
50 <path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"></path>
51 </svg>
52 <h1 className="text-xl">{delta.toLocaleString()}</h1>
53 </CardFooter>
54 </Card>
55 </div>
56 );
57}
Data Processing Utility
Since I have yet to find an effective data manipulation library comparable to Python’s Pandas, data processing is handled using custom objects and functions. The code primarily operates on arrays of Record
s to compute sales metrics and generate visual representations. The getMetrics
and createMetricItems
functions are used to calculate current/delta metrics and construct an array of MetricProp
s that can be added to the Metric component. Also, the createOptionsItems
function is responsible for generating data visualizations, specifically bar charts that show revenue by categories such as country and traffic source.
1// nextjs/src/lib/processing.tsx
2import { MetricProps } from "@/components/metric";
3
4export interface Metrics {
5 num_orders: number;
6 num_order_items: number;
7 total_sales: number;
8}
9
10export interface Record {
11 user_id: string;
12 age: number;
13 gender: string;
14 country: string;
15 traffic_source: string;
16 order_id: string;
17 item_id: string;
18 category: string;
19 item_status: string;
20 sale_price: number;
21 created_at: number;
22}
23
24export const defaultMetrics: Metrics = {
25 num_orders: 0,
26 num_order_items: 0,
27 total_sales: 0,
28};
29
30export const defaultMetricItems: MetricProps[] = [
31 { label: "Number of Orders", value: 0, delta: 0, is_currency: false },
32 { label: "Number of Order Items", value: 0, delta: 0, is_currency: false },
33 { label: "Total Sales", value: 0, delta: 0, is_currency: true },
34];
35
36export function getMetrics(records: Record[]) {
37 const num_orders = [...new Set(records.map((r) => r.order_id))].length;
38 const num_order_items = [...new Set(records.map((r) => r.item_id))].length;
39 const total_sales = Math.round(
40 records.map((r) => Number(r.sale_price)).reduce((a, b) => a + b, 0)
41 );
42 return {
43 num_orders: num_orders,
44 num_order_items: num_order_items,
45 total_sales: total_sales,
46 };
47}
48
49export function createMetricItems(currMetrics: Metrics, prevMetrics: Metrics) {
50 const labels = [
51 { label: "Number of Orders", metric: "num_orders", is_currency: false },
52 {
53 label: "Number of Order Items",
54 metric: "num_order_items",
55 is_currency: false,
56 },
57 { label: "Total Sales", metric: "total_sales", is_currency: true },
58 ];
59 return labels.map((obj) => {
60 const label = obj.label;
61 const value = currMetrics[obj.metric as keyof Metrics];
62 const delta =
63 currMetrics[obj.metric as keyof Metrics] -
64 prevMetrics[obj.metric as keyof Metrics];
65 const is_currency = obj.is_currency;
66 return {
67 label,
68 value,
69 delta,
70 is_currency,
71 };
72 });
73}
74
75export function createOptionsItems(records: Record[]) {
76 const chartCols = [
77 { x: "country", y: "sale_price" },
78 { x: "traffic_source", y: "sale_price" },
79 ];
80 return chartCols.map((col) => {
81 // key is string but it throws the following error. Change the type to 'string | number'.
82 // Argument of type 'string | number' is not assignable to parameter of type 'string'.
83 // Type 'number' is not assignable to type 'string'.ts(2345)
84 const recordsMap = new Map<string | number, number>();
85 for (const r of records) {
86 recordsMap.set(
87 r[col.x as keyof Record],
88 (recordsMap.get(r[col.x as keyof Record]) || 0) +
89 Number(r[col.y as keyof Record])
90 );
91 }
92 const recordsItems = Array.from(recordsMap, ([x, y]) => ({ x, y })).sort(
93 (a, b) => (a.y > b.y ? -1 : 1)
94 );
95 const suffix = col.x
96 .split("_")
97 .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
98 .join(" ");
99 return {
100 title: { text: "Revenue by ".concat(suffix) },
101 yAxis: { type: "value" },
102 xAxis: {
103 type: "category",
104 data: recordsItems.map((r) => r.x),
105 axisLabel: { show: true, rotate: 75 },
106 },
107 series: [
108 {
109 data: recordsItems.map((r) => Math.round(r.y)),
110 type: "bar",
111 colorBy: "data",
112 },
113 ],
114 tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
115 };
116 });
117}
Application
The main component builds a real-time eCommerce dashboard that connects to a WebSocket server at ws://localhost:8000/ws
to fetch and display live data. It uses the React useWebSocket package (react-use-websocket
) to manage the WebSocket connection, and whenever new data is received, it updates the state with the latest metrics and chart options. The data processing is handled by helper functions (getMetrics
, createMetricItems
, and createOptionsItems
), which compute summary metrics and prepare visualization data. The UI dynamically updates to display key business metrics using the Metric component and interactive bar charts powered by Apache ECharts (echarts-for-react
). A checkbox allows users to toggle the WebSocket connection on or off, giving them control over real-time updates.
1// nextjs/src/app/page.tsx
2"use client";
3
4import { useEffect, useState } from "react";
5import { Checkbox } from "@nextui-org/react";
6import ReactECharts, { EChartsOption } from "echarts-for-react";
7import useWebSocket from "react-use-websocket";
8
9import Metric, { MetricProps } from "@/components/metric";
10import {
11 getMetrics,
12 createMetricItems,
13 defaultMetrics,
14 defaultMetricItems,
15 createOptionsItems,
16} from "@/lib/processing";
17
18export default function Home() {
19 const [toConnect, toggleToConnect] = useState(false);
20 const [currMetrics, setCurrMetrics] = useState(defaultMetrics);
21 const [prevMetrics, setPrevMetrics] = useState(defaultMetrics);
22 const [metricItems, setMetricItems] = useState(defaultMetricItems);
23 const [chartOptions, setChartOptions] = useState([] as EChartsOption[]);
24
25 const { lastJsonMessage } = useWebSocket(
26 "ws://localhost:8000/ws",
27 {
28 share: false,
29 shouldReconnect: () => true,
30 },
31 toConnect
32 );
33
34 useEffect(() => {
35 const records = JSON.parse(lastJsonMessage as string);
36 if (!!records) {
37 setPrevMetrics(currMetrics);
38 setCurrMetrics(getMetrics(records));
39 setMetricItems(createMetricItems(currMetrics, prevMetrics));
40 setChartOptions(createOptionsItems(records));
41 }
42 }, [lastJsonMessage]);
43
44 const createMetrics = (metricItems: MetricProps[]) => {
45 return metricItems.map((item, i) => {
46 return (
47 <Metric
48 key={i}
49 label={item.label}
50 value={item.value}
51 delta={item.delta}
52 is_currency={item.is_currency}
53 ></Metric>
54 );
55 });
56 };
57
58 const createCharts = (chartOptions: EChartsOption[]) => {
59 return chartOptions.map((option, i) => {
60 return (
61 <ReactECharts
62 key={i}
63 className="col-span-12 md:col-span-6"
64 option={option}
65 style={{ height: "500px" }}
66 />
67 );
68 });
69 };
70
71 return (
72 <div>
73 <div className="mt-20">
74 <div className="flex m-2 justify-between items-center">
75 <h1 className="text-4xl font-bold">theLook eCommerce Dashboard</h1>
76 </div>
77 <div className="flex m-2 mt-5 justify-between items-center mt-5">
78 <Checkbox
79 color="primary"
80 onChange={() => toggleToConnect(!toConnect)}
81 >
82 Connect to WS Server
83 </Checkbox>
84 ;
85 </div>
86 </div>
87 <div className="grid grid-cols-12 gap-4 mt-5">
88 {createMetrics(metricItems)}
89 </div>
90 <div className="grid grid-cols-12 gap-4 mt-5">
91 {createCharts(chartOptions)}
92 </div>
93 </div>
94 );
95}
Deployment
Data Producer and WebSocket Server
As discussed in Part 1, the data generator and WebSocket server can be deployed using Docker Compose with the command docker-compose -f producer/docker-compose.yml up -d
. Once started, the server can be checked with a WebSocket client by executing ws listen ws://localhost:8000/ws
, and its logs can be monitored by running docker logs -f producer
.
Frontend Dashboard
The dashboard can be started in development mode as shown below. Once started, it can be accessed in a browser at http://localhost:3000.
1## install pnpm if not done
2# https://pnpm.io/installation
3
4## install dependent packages
5$ pnpm install
6
7## start the app
8$ pnpm dev
Comments