From 368d07017424d48c696c6627c402cd196e996f08 Mon Sep 17 00:00:00 2001 From: Dex Date: Tue, 26 May 2026 14:15:25 +0000 Subject: [PATCH] CUB-134: Build printers list page --- frontend/src/App.tsx | 15 +- frontend/src/pages/PrintersPage.tsx | 262 ++++++++++++++++++++++++ frontend/src/services/printerService.ts | 9 + frontend/src/types/printer.ts | 29 +++ 4 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/PrintersPage.tsx create mode 100644 frontend/src/services/printerService.ts create mode 100644 frontend/src/types/printer.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d755456..6ef152e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { BrowserRouter, Routes, Route } from 'react-router-dom' import Dashboard from './pages/Dashboard' import InventoryPage from './pages/InventoryPage' +import PrintersPage from './pages/PrintersPage' const queryClient = new QueryClient() @@ -10,14 +11,22 @@ export default function App() {
-
-
E
-

Extrudex

+
+ +
E
+

Extrudex

+
+
} /> } /> + } />
diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx new file mode 100644 index 0000000..2a27848 --- /dev/null +++ b/frontend/src/pages/PrintersPage.tsx @@ -0,0 +1,262 @@ +import { useQuery } from '@tanstack/react-query' +import { Printer, Wifi, WifiOff, HelpCircle, Plus, Globe, Radio } from 'lucide-react' +import { fetchPrinters } from '../services/printerService' +import LoadingSpinner from '../components/LoadingSpinner' +import type { Printer as PrinterType, ConnectionStatus } from '../types/printer' + +function getConnectionStatus(printer: PrinterType): ConnectionStatus { + if (!printer.is_active) return 'offline' + if (printer.moonraker_url || printer.mqtt_broker_host) return 'online' + return 'unknown' +} + +const statusConfig: Record = { + online: { + icon: Wifi, + label: 'Online', + className: 'bg-emerald-900/30 border-emerald-700 text-emerald-300', + }, + offline: { + icon: WifiOff, + label: 'Offline', + className: 'bg-red-900/30 border-red-700 text-red-300', + }, + unknown: { + icon: HelpCircle, + label: 'Unknown', + className: 'bg-slate-700/50 border-slate-600 text-slate-400', + }, +} + +export default function PrintersPage() { + const { data: printers, isLoading, error, refetch } = useQuery({ + queryKey: ['printers'], + queryFn: fetchPrinters, + }) + + const count = printers?.length ?? 0 + + return ( +
+ {/* Header */} +
+
+

Printers

+

{count} printer(s) in fleet

+
+ +
+ + {/* Loading */} + {isLoading && ( +
+ +
+ )} + + {/* Error */} + {error && ( +
+

Failed to load printers.

+ +
+ )} + + {/* Empty */} + {!isLoading && !error && printers?.length === 0 && ( +
+ +

No printers found.

+

+ Add a printer to start monitoring your fleet. +

+
+ )} + + {/* Desktop Table */} + {!isLoading && !error && (printers?.length ?? 0) > 0 && ( + <> +
+ + + + + + + + + + + + + + {printers!.map((printer) => { + const status = getConnectionStatus(printer) + const StatusIcon = statusConfig[status].icon + return ( + + + + + + + + + + ) + })} + +
NameTypeManufacturerModelStatusMoonrakerMQTT
+ {printer.name} + + {printer.printer_type?.name ?? '—'} + + {printer.manufacturer || '—'} + + {printer.model || '—'} + + + + {statusConfig[status].label} + + + {printer.moonraker_url ? ( + + + {printer.moonraker_url} + + ) : ( + + )} + + {printer.mqtt_broker_host ? ( +
+
+ + + {printer.mqtt_broker_host} + +
+ {printer.mqtt_topic_prefix && ( +
+ topic: {printer.mqtt_topic_prefix} +
+ )} +
+ TLS: {printer.mqtt_tls_enabled ? 'on' : 'off'} +
+
+ ) : ( + + )} +
+
+ + {/* Mobile / Tablet Cards */} +
+ {printers!.map((printer) => { + const status = getConnectionStatus(printer) + const StatusIcon = statusConfig[status].icon + return ( +
+ {/* Top row: name + status */} +
+
+
+ {printer.name} +
+
+ {[ + printer.printer_type?.name, + printer.manufacturer, + printer.model, + ] + .filter(Boolean) + .join(' · ') || 'No details'} +
+
+ + + {statusConfig[status].label} + +
+ + {/* Connection details */} +
+ {/* Moonraker */} +
+
+ + Moonraker +
+ {printer.moonraker_url ? ( + + {printer.moonraker_url} + + ) : ( + Not configured + )} +
+ + {/* MQTT */} +
+
+ + MQTT +
+ {printer.mqtt_broker_host ? ( +
+
+ {printer.mqtt_broker_host} +
+ {printer.mqtt_topic_prefix && ( +
+ topic: {printer.mqtt_topic_prefix} +
+ )} +
+ TLS: {printer.mqtt_tls_enabled ? 'on' : 'off'} +
+
+ ) : ( + Not configured + )} +
+
+
+ ) + })} +
+ + )} +
+ ) +} diff --git a/frontend/src/services/printerService.ts b/frontend/src/services/printerService.ts new file mode 100644 index 0000000..b46262b --- /dev/null +++ b/frontend/src/services/printerService.ts @@ -0,0 +1,9 @@ +import axios from 'axios' +import type { Printer, PrinterResponse } from '../types/printer' + +const API_BASE = '/api' + +export async function fetchPrinters(): Promise { + const res = await axios.get(`${API_BASE}/printers`) + return res.data.data +} diff --git a/frontend/src/types/printer.ts b/frontend/src/types/printer.ts new file mode 100644 index 0000000..317391b --- /dev/null +++ b/frontend/src/types/printer.ts @@ -0,0 +1,29 @@ +export interface PrinterType { + id: number + name: string + created_at: string + updated_at: string +} + +export interface Printer { + id: number + name: string + printer_type_id: number + printer_type?: PrinterType + manufacturer?: string + model?: string + moonraker_url?: string + moonraker_api_key?: string + mqtt_broker_host?: string + mqtt_topic_prefix?: string + mqtt_tls_enabled: boolean + is_active: boolean + created_at: string + updated_at: string +} + +export type ConnectionStatus = 'online' | 'offline' | 'unknown' + +export interface PrinterResponse { + data: Printer[] +}