Browse Source

Fixed bug in MQTT debug viewer; Added filter and search for MQTT messages

maziggy 5 months ago
parent
commit
0c452a12e9

+ 146 - 55
frontend/src/components/MQTTDebugModal.tsx

@@ -1,8 +1,8 @@
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { X, Play, Square, Trash2, RefreshCw, ArrowDown, ArrowUp } from 'lucide-react';
+import { X, Play, Square, Trash2, RefreshCw, ArrowDown, ArrowUp, Search } from 'lucide-react';
 import { api, type MQTTLogEntry } from '../api/client';
 import { api, type MQTTLogEntry } from '../api/client';
 import { Button } from './Button';
 import { Button } from './Button';
-import { useState, useEffect, useRef } from 'react';
+import { useState, useEffect, useRef, useMemo } from 'react';
 
 
 interface MQTTDebugModalProps {
 interface MQTTDebugModalProps {
   printerId: number;
   printerId: number;
@@ -14,6 +14,8 @@ export function MQTTDebugModal({ printerId, printerName, onClose }: MQTTDebugMod
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const [autoScroll, setAutoScroll] = useState(true);
   const [autoScroll, setAutoScroll] = useState(true);
   const [expandedLogs, setExpandedLogs] = useState<Set<number>>(new Set());
   const [expandedLogs, setExpandedLogs] = useState<Set<number>>(new Set());
+  const [searchQuery, setSearchQuery] = useState('');
+  const [directionFilter, setDirectionFilter] = useState<'all' | 'in' | 'out'>('all');
   const logContainerRef = useRef<HTMLDivElement>(null);
   const logContainerRef = useRef<HTMLDivElement>(null);
 
 
   const { data, isLoading, refetch } = useQuery({
   const { data, isLoading, refetch } = useQuery({
@@ -76,8 +78,13 @@ export function MQTTDebugModal({ printerId, printerName, onClose }: MQTTDebugMod
     return date.toLocaleTimeString('en-US', { hour12: false, fractionalSecondDigits: 3 });
     return date.toLocaleTimeString('en-US', { hour12: false, fractionalSecondDigits: 3 });
   };
   };
 
 
-  const formatPayload = (payload: Record<string, unknown>, expanded: boolean) => {
-    const json = JSON.stringify(payload, null, expanded ? 2 : 0);
+  const formatPayload = (payload: unknown, expanded: boolean): string => {
+    if (payload === undefined || payload === null) {
+      return '<empty>';
+    }
+    // If payload is already a string, parse it first to format nicely
+    const obj = typeof payload === 'string' ? JSON.parse(payload) : payload;
+    const json = JSON.stringify(obj, null, expanded ? 2 : 0);
     if (!expanded && json.length > 100) {
     if (!expanded && json.length > 100) {
       return json.substring(0, 100) + '...';
       return json.substring(0, 100) + '...';
     }
     }
@@ -87,6 +94,25 @@ export function MQTTDebugModal({ printerId, printerName, onClose }: MQTTDebugMod
   const loggingEnabled = data?.logging_enabled ?? false;
   const loggingEnabled = data?.logging_enabled ?? false;
   const logs = data?.logs ?? [];
   const logs = data?.logs ?? [];
 
 
+  // Filter logs based on search query and direction filter
+  const filteredLogs = useMemo(() => {
+    return logs.filter((log) => {
+      // Direction filter
+      if (directionFilter !== 'all' && log.direction !== directionFilter) {
+        return false;
+      }
+      // Search filter
+      if (searchQuery.trim()) {
+        const query = searchQuery.toLowerCase();
+        const topicMatch = log.topic.toLowerCase().includes(query);
+        const payloadStr = JSON.stringify(log.payload).toLowerCase();
+        const payloadMatch = payloadStr.includes(query);
+        return topicMatch || payloadMatch;
+      }
+      return true;
+    });
+  }, [logs, searchQuery, directionFilter]);
+
   return (
   return (
     <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
     <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
       <div className="bg-bambu-dark-secondary rounded-lg max-w-4xl w-full max-h-[85vh] flex flex-col">
       <div className="bg-bambu-dark-secondary rounded-lg max-w-4xl w-full max-h-[85vh] flex flex-col">
@@ -105,63 +131,121 @@ export function MQTTDebugModal({ printerId, printerName, onClose }: MQTTDebugMod
         </div>
         </div>
 
 
         {/* Controls */}
         {/* Controls */}
-        <div className="flex items-center gap-2 p-4 border-b border-bambu-dark-tertiary">
-          {loggingEnabled ? (
+        <div className="flex flex-col gap-2 p-4 border-b border-bambu-dark-tertiary">
+          <div className="flex items-center gap-2">
+            {loggingEnabled ? (
+              <Button
+                size="sm"
+                variant="secondary"
+                onClick={() => disableMutation.mutate()}
+                disabled={disableMutation.isPending}
+              >
+                <Square className="w-4 h-4" />
+                Stop
+              </Button>
+            ) : (
+              <Button
+                size="sm"
+                onClick={() => enableMutation.mutate()}
+                disabled={enableMutation.isPending}
+              >
+                <Play className="w-4 h-4" />
+                Start Logging
+              </Button>
+            )}
             <Button
             <Button
               size="sm"
               size="sm"
               variant="secondary"
               variant="secondary"
-              onClick={() => disableMutation.mutate()}
-              disabled={disableMutation.isPending}
+              onClick={() => clearMutation.mutate()}
+              disabled={clearMutation.isPending || logs.length === 0}
             >
             >
-              <Square className="w-4 h-4" />
-              Stop
+              <Trash2 className="w-4 h-4" />
+              Clear
             </Button>
             </Button>
-          ) : (
             <Button
             <Button
               size="sm"
               size="sm"
-              onClick={() => enableMutation.mutate()}
-              disabled={enableMutation.isPending}
+              variant="secondary"
+              onClick={() => refetch()}
+              disabled={isLoading}
             >
             >
-              <Play className="w-4 h-4" />
-              Start Logging
+              <RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
             </Button>
             </Button>
-          )}
-          <Button
-            size="sm"
-            variant="secondary"
-            onClick={() => clearMutation.mutate()}
-            disabled={clearMutation.isPending || logs.length === 0}
-          >
-            <Trash2 className="w-4 h-4" />
-            Clear
-          </Button>
-          <Button
-            size="sm"
-            variant="secondary"
-            onClick={() => refetch()}
-            disabled={isLoading}
-          >
-            <RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
-          </Button>
-          <div className="flex-1" />
-          <label className="flex items-center gap-2 text-sm text-bambu-gray cursor-pointer">
-            <input
-              type="checkbox"
-              checked={autoScroll}
-              onChange={(e) => setAutoScroll(e.target.checked)}
-              className="rounded border-bambu-dark-tertiary"
-            />
-            Auto-scroll
-          </label>
-          <span className="text-sm text-bambu-gray">
-            {logs.length} message{logs.length !== 1 ? 's' : ''}
-          </span>
+            <div className="flex-1" />
+            <label className="flex items-center gap-2 text-sm text-bambu-gray cursor-pointer">
+              <input
+                type="checkbox"
+                checked={autoScroll}
+                onChange={(e) => setAutoScroll(e.target.checked)}
+                className="rounded border-bambu-dark-tertiary"
+              />
+              Auto-scroll
+            </label>
+            <span className="text-sm text-bambu-gray">
+              {filteredLogs.length}/{logs.length}
+            </span>
+          </div>
+
+          {/* Search and Filter Row */}
+          <div className="flex items-center gap-2">
+            <div className="relative flex-1">
+              <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray" />
+              <input
+                type="text"
+                placeholder="Search topic or payload..."
+                value={searchQuery}
+                onChange={(e) => setSearchQuery(e.target.value)}
+                className="w-full pl-8 pr-3 py-1.5 text-sm bg-bambu-dark border border-bambu-dark-tertiary rounded text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
+              />
+              {searchQuery && (
+                <button
+                  onClick={() => setSearchQuery('')}
+                  className="absolute right-2 top-1/2 -translate-y-1/2 text-bambu-gray hover:text-white"
+                >
+                  <X className="w-4 h-4" />
+                </button>
+              )}
+            </div>
+            <div className="flex items-center gap-1 bg-bambu-dark rounded border border-bambu-dark-tertiary">
+              <button
+                onClick={() => setDirectionFilter('all')}
+                className={`px-2 py-1.5 text-xs rounded-l transition-colors ${
+                  directionFilter === 'all'
+                    ? 'bg-bambu-green text-white'
+                    : 'text-bambu-gray hover:text-white'
+                }`}
+              >
+                All
+              </button>
+              <button
+                onClick={() => setDirectionFilter('in')}
+                className={`px-2 py-1.5 text-xs transition-colors flex items-center gap-1 ${
+                  directionFilter === 'in'
+                    ? 'bg-blue-500 text-white'
+                    : 'text-bambu-gray hover:text-white'
+                }`}
+              >
+                <ArrowDown className="w-3 h-3" />
+                In
+              </button>
+              <button
+                onClick={() => setDirectionFilter('out')}
+                className={`px-2 py-1.5 text-xs rounded-r transition-colors flex items-center gap-1 ${
+                  directionFilter === 'out'
+                    ? 'bg-green-500 text-white'
+                    : 'text-bambu-gray hover:text-white'
+                }`}
+              >
+                <ArrowUp className="w-3 h-3" />
+                Out
+              </button>
+            </div>
+          </div>
         </div>
         </div>
 
 
         {/* Log Content */}
         {/* Log Content */}
         <div
         <div
           ref={logContainerRef}
           ref={logContainerRef}
-          className="flex-1 overflow-auto p-4 font-mono text-xs bg-bambu-dark min-h-[400px]"
+          className="flex-1 overflow-auto p-4 font-mono text-xs bg-black min-h-[400px]"
         >
         >
           {logs.length === 0 ? (
           {logs.length === 0 ? (
             <div className="flex flex-col items-center justify-center h-full text-bambu-gray">
             <div className="flex flex-col items-center justify-center h-full text-bambu-gray">
@@ -170,9 +254,14 @@ export function MQTTDebugModal({ printerId, printerName, onClose }: MQTTDebugMod
                 <p className="text-sm">Click "Start Logging" to begin capturing MQTT messages</p>
                 <p className="text-sm">Click "Start Logging" to begin capturing MQTT messages</p>
               )}
               )}
             </div>
             </div>
+          ) : filteredLogs.length === 0 ? (
+            <div className="flex flex-col items-center justify-center h-full text-bambu-gray">
+              <p className="mb-2">No messages match your filter</p>
+              <p className="text-sm">Try adjusting your search or filter criteria</p>
+            </div>
           ) : (
           ) : (
             <div className="space-y-1">
             <div className="space-y-1">
-              {logs.map((log: MQTTLogEntry, index: number) => {
+              {filteredLogs.map((log: MQTTLogEntry, index: number) => {
                 const isExpanded = expandedLogs.has(index);
                 const isExpanded = expandedLogs.has(index);
                 const isIncoming = log.direction === 'in';
                 const isIncoming = log.direction === 'in';
 
 
@@ -202,13 +291,15 @@ export function MQTTDebugModal({ printerId, printerName, onClose }: MQTTDebugMod
                       </span>
                       </span>
                       <span className="text-purple-400 shrink-0">{log.topic}</span>
                       <span className="text-purple-400 shrink-0">{log.topic}</span>
                     </div>
                     </div>
-                    <pre
-                      className={`mt-1 text-white/80 overflow-x-auto ${
-                        isExpanded ? 'whitespace-pre-wrap' : 'truncate'
-                      }`}
-                    >
-                      {formatPayload(log.payload, isExpanded)}
-                    </pre>
+                    {isExpanded ? (
+                      <pre className="mt-2 p-3 bg-gray-900 border border-gray-700 rounded text-green-400 overflow-x-auto whitespace-pre-wrap break-all max-h-96 overflow-y-auto text-xs">
+                        {formatPayload(log.payload, true)}
+                      </pre>
+                    ) : (
+                      <pre className="mt-1 text-white/80 truncate">
+                        {formatPayload(log.payload, false)}
+                      </pre>
+                    )}
                   </div>
                   </div>
                 );
                 );
               })}
               })}

+ 177 - 0
frontend/src/pages/PrintersPage.tsx

@@ -18,6 +18,7 @@ import {
   Zap,
   Zap,
   Wrench,
   Wrench,
   ChevronDown,
   ChevronDown,
+  Pencil,
 } from 'lucide-react';
 } from 'lucide-react';
 import { useNavigate } from 'react-router-dom';
 import { useNavigate } from 'react-router-dom';
 import { api } from '../api/client';
 import { api } from '../api/client';
@@ -104,6 +105,7 @@ function PrinterCard({
   const navigate = useNavigate();
   const navigate = useNavigate();
   const [showMenu, setShowMenu] = useState(false);
   const [showMenu, setShowMenu] = useState(false);
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+  const [showEditModal, setShowEditModal] = useState(false);
   const [showFileManager, setShowFileManager] = useState(false);
   const [showFileManager, setShowFileManager] = useState(false);
   const [showMQTTDebug, setShowMQTTDebug] = useState(false);
   const [showMQTTDebug, setShowMQTTDebug] = useState(false);
   const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false);
   const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false);
@@ -254,6 +256,16 @@ function PrinterCard({
               </Button>
               </Button>
               {showMenu && (
               {showMenu && (
                 <div className="absolute right-0 mt-2 w-48 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg shadow-lg z-10">
                 <div className="absolute right-0 mt-2 w-48 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg shadow-lg z-10">
+                  <button
+                    className="w-full px-4 py-2 text-left text-sm hover:bg-bambu-dark-tertiary flex items-center gap-2"
+                    onClick={() => {
+                      setShowEditModal(true);
+                      setShowMenu(false);
+                    }}
+                  >
+                    <Pencil className="w-4 h-4" />
+                    Edit
+                  </button>
                   <button
                   <button
                     className="w-full px-4 py-2 text-left text-sm hover:bg-bambu-dark-tertiary flex items-center gap-2"
                     className="w-full px-4 py-2 text-left text-sm hover:bg-bambu-dark-tertiary flex items-center gap-2"
                     onClick={() => {
                     onClick={() => {
@@ -566,6 +578,14 @@ function PrinterCard({
           onClose={() => setShowHMSModal(false)}
           onClose={() => setShowHMSModal(false)}
         />
         />
       )}
       )}
+
+      {/* Edit Printer Modal */}
+      {showEditModal && (
+        <EditPrinterModal
+          printer={printer}
+          onClose={() => setShowEditModal(false)}
+        />
+      )}
     </Card>
     </Card>
   );
   );
 }
 }
@@ -711,6 +731,163 @@ function AddPrinterModal({
   );
   );
 }
 }
 
 
+function EditPrinterModal({
+  printer,
+  onClose,
+}: {
+  printer: Printer;
+  onClose: () => void;
+}) {
+  const queryClient = useQueryClient();
+  const [form, setForm] = useState({
+    name: printer.name,
+    ip_address: printer.ip_address,
+    access_code: '',
+    model: printer.model || '',
+    auto_archive: printer.auto_archive,
+  });
+
+  const updateMutation = useMutation({
+    mutationFn: (data: Partial<PrinterCreate>) => api.updatePrinter(printer.id, data),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['printers'] });
+      queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] });
+      onClose();
+    },
+  });
+
+  // Close on Escape key
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    const data: Partial<PrinterCreate> = {
+      name: form.name,
+      ip_address: form.ip_address,
+      model: form.model || undefined,
+      auto_archive: form.auto_archive,
+    };
+    // Only include access_code if it was changed
+    if (form.access_code) {
+      data.access_code = form.access_code;
+    }
+    updateMutation.mutate(data);
+  };
+
+  return (
+    <div
+      className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
+      onClick={onClose}
+    >
+      <Card className="w-full max-w-md" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
+        <CardContent>
+          <h2 className="text-xl font-semibold mb-4">Edit Printer</h2>
+          <form onSubmit={handleSubmit} className="space-y-4">
+            <div>
+              <label className="block text-sm text-bambu-gray mb-1">Name</label>
+              <input
+                type="text"
+                required
+                className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
+                value={form.name}
+                onChange={(e) => setForm({ ...form, name: e.target.value })}
+                placeholder="My Printer"
+              />
+            </div>
+            <div>
+              <label className="block text-sm text-bambu-gray mb-1">IP Address</label>
+              <input
+                type="text"
+                required
+                pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
+                className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
+                value={form.ip_address}
+                onChange={(e) => setForm({ ...form, ip_address: e.target.value })}
+                placeholder="192.168.1.100"
+              />
+            </div>
+            <div>
+              <label className="block text-sm text-bambu-gray mb-1">Serial Number</label>
+              <input
+                type="text"
+                disabled
+                className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray cursor-not-allowed"
+                value={printer.serial_number}
+              />
+              <p className="text-xs text-bambu-gray mt-1">Serial number cannot be changed</p>
+            </div>
+            <div>
+              <label className="block text-sm text-bambu-gray mb-1">Access Code</label>
+              <input
+                type="password"
+                className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
+                value={form.access_code}
+                onChange={(e) => setForm({ ...form, access_code: e.target.value })}
+                placeholder="Leave empty to keep current"
+              />
+            </div>
+            <div>
+              <label className="block text-sm text-bambu-gray mb-1">Model</label>
+              <select
+                className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
+                value={form.model}
+                onChange={(e) => setForm({ ...form, model: e.target.value })}
+              >
+                <option value="">Select model...</option>
+                <optgroup label="H2 Series">
+                  <option value="H2C">H2C</option>
+                  <option value="H2D">H2D</option>
+                  <option value="H2S">H2S</option>
+                </optgroup>
+                <optgroup label="X1 Series">
+                  <option value="X1E">X1E</option>
+                  <option value="X1C">X1 Carbon</option>
+                  <option value="X1">X1</option>
+                </optgroup>
+                <optgroup label="P Series">
+                  <option value="P2S">P2S</option>
+                  <option value="P1S">P1S</option>
+                  <option value="P1P">P1P</option>
+                </optgroup>
+                <optgroup label="A1 Series">
+                  <option value="A1">A1</option>
+                  <option value="A1 Mini">A1 Mini</option>
+                </optgroup>
+              </select>
+            </div>
+            <div className="flex items-center gap-2">
+              <input
+                type="checkbox"
+                id="edit_auto_archive"
+                checked={form.auto_archive}
+                onChange={(e) => setForm({ ...form, auto_archive: e.target.checked })}
+                className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
+              />
+              <label htmlFor="edit_auto_archive" className="text-sm text-bambu-gray">
+                Auto-archive completed prints
+              </label>
+            </div>
+            <div className="flex gap-3 pt-4">
+              <Button type="button" variant="secondary" onClick={onClose} className="flex-1">
+                Cancel
+              </Button>
+              <Button type="submit" className="flex-1" disabled={updateMutation.isPending}>
+                {updateMutation.isPending ? 'Saving...' : 'Save Changes'}
+              </Button>
+            </div>
+          </form>
+        </CardContent>
+      </Card>
+    </div>
+  );
+}
+
 // Component to check if a printer is offline (for power dropdown)
 // Component to check if a printer is offline (for power dropdown)
 function usePrinterOfflineStatus(printerId: number) {
 function usePrinterOfflineStatus(printerId: number) {
   const { data: status } = useQuery({
   const { data: status } = useQuery({

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-B01KJJLK.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BFqTr5zP.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-Cx4S-qVJ.css


+ 2 - 2
static/index.html

@@ -7,8 +7,8 @@
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
-    <script type="module" crossorigin src="/assets/index-zCm77ErN.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-Cx4S-qVJ.css">
+    <script type="module" crossorigin src="/assets/index-BFqTr5zP.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-B01KJJLK.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff