Xây dựng Web Dashboard với NiceGUI: Real-time UI hoàn toàn bằng Python

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Hồi đầu năm ngoái, mình cần build một dashboard nội bộ để team DevOps theo dõi trạng thái server và log deployment. Thử qua Streamlit thì bị giới hạn layout, Dash thì callback pattern phức tạp hơn tưởng, còn Flask + Jinja thì lại phải viết thêm JavaScript — điều mình không muốn khi đang bận code Python.

Sau một hồi tìm kiếm, mình thử NiceGUI và đã chạy production được 6 tháng. Kết quả: dashboard chạy ổn, team hài lòng, và mình không cần viết một dòng JavaScript nào.

NiceGUI là gì và tại sao nó khác các giải pháp khác?

NiceGUI là thư viện Python cho phép xây dựng giao diện web — dashboard, form, chat UI, monitoring tool — hoàn toàn bằng Python. Bên dưới nó dùng FastAPI làm web server và Vue.js + Quasar làm frontend framework, nhưng bạn không cần biết điều này khi code.

Điểm khác biệt so với các tool tương tự:

  • Streamlit: Re-render toàn bộ trang mỗi khi có thay đổi — không phù hợp với real-time data.
  • Dash: Callback pattern phức tạp, khó debug khi app lớn dần.
  • Gradio: Tập trung vào ML demo, không phải dashboard đa năng.
  • NiceGUI: Reactive UI, real-time update qua WebSocket, layout linh hoạt, component phong phú.

NiceGUI hoạt động theo mô hình event-driven: bạn define UI components trong Python, bind chúng với data hoặc function, và khi data thay đổi, UI tự cập nhật mà không cần reload trang.

Cài đặt và bắt đầu

Cài NiceGUI đơn giản:

pip install nicegui

Ứng dụng Hello World đầu tiên:

from nicegui import ui

ui.label('Hello, NiceGUI!')
ui.button('Click me', on_click=lambda: ui.notify('Bạn vừa click!'))

ui.run()

Chạy file này, mở http://localhost:8080 là xong. NiceGUI tự lo phần server, tự reload khi code thay đổi ở dev mode.

Thực hành: Dashboard giám sát server real-time

Dưới đây là ví dụ thực tế — dashboard hiển thị CPU, RAM và disk usage, cập nhật mỗi 2 giây:

pip install nicegui psutil
import psutil
from nicegui import ui

# Cards hiển thị metrics
with ui.row().classes('w-full gap-4 p-4'):
    with ui.card().classes('flex-1'):
        ui.label('CPU Usage').classes('text-lg font-bold')
        cpu_label = ui.label('0%').classes('text-3xl text-blue-500')

    with ui.card().classes('flex-1'):
        ui.label('RAM Usage').classes('text-lg font-bold')
        ram_label = ui.label('0%').classes('text-3xl text-green-500')

    with ui.card().classes('flex-1'):
        ui.label('Disk Usage').classes('text-lg font-bold')
        disk_label = ui.label('0%').classes('text-3xl text-orange-500')

# Chart CPU history
with ui.card().classes('w-full p-4'):
    ui.label('CPU History (60s)').classes('font-bold mb-2')
    cpu_chart = ui.chart({
        'title': False,
        'chart': {'type': 'line'},
        'series': [{'name': 'CPU %', 'data': [0] * 60}],
        'xAxis': {'visible': False},
        'yAxis': {'min': 0, 'max': 100},
    }).classes('w-full h-64')

cpu_history = [0] * 60

def update_metrics():
    cpu = psutil.cpu_percent()
    ram = psutil.virtual_memory().percent
    disk = psutil.disk_usage('/').percent

    cpu_label.set_text(f'{cpu:.1f}%')
    ram_label.set_text(f'{ram:.1f}%')
    disk_label.set_text(f'{disk:.1f}%')

    cpu_history.pop(0)
    cpu_history.append(cpu)
    cpu_chart.options['series'][0]['data'] = cpu_history
    cpu_chart.update()

ui.timer(2.0, update_metrics)
ui.run(title='Server Monitor', port=8080)

ui.timer() gọi update_metrics() mỗi 2 giây — NiceGUI tự push update xuống browser qua WebSocket, không cần polling từ phía client. Đây là điểm mạnh nhất so với Streamlit.

Table server với sort

from nicegui import ui

columns = [
    {'name': 'server', 'label': 'Server', 'field': 'server', 'sortable': True},
    {'name': 'status', 'label': 'Status', 'field': 'status'},
    {'name': 'uptime', 'label': 'Uptime', 'field': 'uptime', 'sortable': True},
]

rows = [
    {'server': 'web-01', 'status': '✅ Online',   'uptime': '15d 3h'},
    {'server': 'web-02', 'status': '✅ Online',   'uptime': '7d 11h'},
    {'server': 'db-01',  'status': '⚠️ Warning', 'uptime': '30d 2h'},
]

ui.table(columns=columns, rows=rows, row_key='server').classes('w-full')
ui.run()

Form nhập liệu với validation

from nicegui import ui

with ui.card().classes('p-4 w-96'):
    ui.label('Thêm server mới').classes('text-xl font-bold mb-4')

    hostname = ui.input('Hostname', placeholder='web-01.example.com').classes('w-full')
    port = ui.number('Port', value=22, min=1, max=65535).classes('w-full')
    env = ui.select(
        label='Environment',
        options=['production', 'staging', 'development'],
        value='production'
    ).classes('w-full')

    def submit():
        if not hostname.value:
            ui.notify('Hostname không được để trống!', type='warning')
            return
        ui.notify(f'Đã thêm {hostname.value}:{port.value} ({env.value})', type='positive')

    ui.button('Thêm', on_click=submit).classes('w-full mt-4')

ui.run()

Parse log với regex — trick nhỏ tiết kiệm thời gian

Phần hay gặp khó nhất trong dashboard log viewer là viết đúng regex pattern để extract thông tin từ log line. Mình hay test nhanh pattern trên toolcraft.app/vi/tools/developer/regex-tester trước — paste vài dòng log thật vào, thấy match đúng mới copy vào code Python. Nhanh hơn nhiều so với chạy đi chạy lại script để debug.

Ví dụ parse nginx access log và hiển thị lên NiceGUI table:

import re
from nicegui import ui

LOG_PATTERN = re.compile(
    r'(?P<ip>\d+\.\d+\.\d+\.\d+) .+ \[(?P<time>[^\]]+)\] '
    r'"(?P<method>\w+) (?P<path>[^ ]+) HTTP/[\d.]+" '
    r'(?P<status>\d+) (?P<bytes>\d+)'
)

def parse_log_line(line: str) -> dict | None:
    m = LOG_PATTERN.match(line)
    return m.groupdict() if m else None

sample_logs = [
    '192.168.1.1 - - [15/Jul/2026:10:00:01 +0900] "GET /api/status HTTP/1.1" 200 1234',
    '10.0.0.2 - - [15/Jul/2026:10:00:05 +0900] "POST /api/deploy HTTP/1.1" 201 567',
]

entries = [e for l in sample_logs if (e := parse_log_line(l))]

columns = [
    {'name': 'ip',     'label': 'IP',     'field': 'ip'},
    {'name': 'method', 'label': 'Method', 'field': 'method'},
    {'name': 'path',   'label': 'Path',   'field': 'path'},
    {'name': 'status', 'label': 'Status', 'field': 'status'},
]

ui.table(columns=columns, rows=entries, row_key='path').classes('w-full')
ui.run()

Deploy production

NiceGUI chạy sau nginx như một FastAPI app thông thường. Cần thêm WebSocket support trong nginx config:

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}

Và systemd service để tự restart khi crash:

[Unit]
Description=NiceGUI Dashboard
After=network.target

[Service]
User=ubuntu
WorkingDirectory=/opt/dashboard
ExecStart=/opt/dashboard/venv/bin/python main.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable dashboard
sudo systemctl start dashboard

Kết luận: Nên dùng NiceGUI khi nào?

Sau 6 tháng dùng thực tế, mình có thể nói thẳng: NiceGUI phù hợp nhất cho internal tooldashboard nội bộ — những thứ cần build nhanh, cần real-time update, và team không có frontend engineer riêng. Nếu cần xây SaaS với UX phức tạp và animation nhiều, thì vẫn nên dùng React hoặc Vue.

Điểm mạnh thực sự:

  • Toàn bộ logic trong Python — không context-switch sang JS/CSS
  • Real-time update qua WebSocket sẵn có, không cần cấu hình thêm
  • Component library đầy đủ: chart, table, form, dialog, notification
  • Tailwind CSS classes dùng trực tiếp qua .classes()
  • Tích hợp được vào FastAPI app có sẵn nếu cần mở rộng

Điểm cần lưu ý: app có nhiều user đồng thời cần chú ý về state management vì mỗi connection là một session riêng. Ecosystem plugin cũng chưa rộng bằng Streamlit. Nhưng nếu bạn đang tìm cách build dashboard nhanh cho team mà không muốn học thêm framework JS nào, NiceGUI là lựa chọn xứng đáng để thử.

Share: