Browse Source

Added power switch dropdown to printer card for offline printers

maziggy 5 months ago
parent
commit
29327d0f16

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

@@ -17,6 +17,7 @@ import {
   PowerOff,
   Zap,
   Wrench,
+  ChevronDown,
 } from 'lucide-react';
 import { useNavigate } from 'react-router-dom';
 import { api } from '../api/client';
@@ -710,11 +711,81 @@ function AddPrinterModal({
   );
 }
 
+// Component to check if a printer is offline (for power dropdown)
+function usePrinterOfflineStatus(printerId: number) {
+  const { data: status } = useQuery({
+    queryKey: ['printerStatus', printerId],
+    queryFn: () => api.getPrinterStatus(printerId),
+    refetchInterval: 30000,
+  });
+  return !status?.connected;
+}
+
+// Power dropdown item for an offline printer
+function PowerDropdownItem({
+  printer,
+  plug,
+  onPowerOn,
+  isPowering,
+}: {
+  printer: Printer;
+  plug: { id: number; name: string };
+  onPowerOn: (plugId: number) => void;
+  isPowering: boolean;
+}) {
+  const isOffline = usePrinterOfflineStatus(printer.id);
+
+  // Fetch plug status
+  const { data: plugStatus } = useQuery({
+    queryKey: ['smartPlugStatus', plug.id],
+    queryFn: () => api.getSmartPlugStatus(plug.id),
+    refetchInterval: 10000,
+  });
+
+  // Only show if printer is offline
+  if (!isOffline) {
+    return null;
+  }
+
+  return (
+    <div className="flex items-center justify-between px-3 py-2 hover:bg-gray-100 dark:hover:bg-bambu-dark-tertiary">
+      <div className="flex items-center gap-2 min-w-0">
+        <span className="text-sm text-gray-900 dark:text-white truncate">{printer.name}</span>
+        {plugStatus && (
+          <span
+            className={`text-xs px-1.5 py-0.5 rounded ${
+              plugStatus.state === 'ON'
+                ? 'bg-bambu-green/20 text-bambu-green'
+                : 'bg-red-500/20 text-red-400'
+            }`}
+          >
+            {plugStatus.state || '?'}
+          </span>
+        )}
+      </div>
+      <button
+        onClick={() => onPowerOn(plug.id)}
+        disabled={isPowering || plugStatus?.state === 'ON'}
+        className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
+          plugStatus?.state === 'ON'
+            ? 'bg-bambu-green/20 text-bambu-green cursor-default'
+            : 'bg-bambu-green/20 text-bambu-green hover:bg-bambu-green hover:text-white'
+        }`}
+      >
+        <Power className="w-3 h-3" />
+        {isPowering ? '...' : 'On'}
+      </button>
+    </div>
+  );
+}
+
 export function PrintersPage() {
   const [showAddModal, setShowAddModal] = useState(false);
   const [hideDisconnected, setHideDisconnected] = useState(() => {
     return localStorage.getItem('hideDisconnectedPrinters') === 'true';
   });
+  const [showPowerDropdown, setShowPowerDropdown] = useState(false);
+  const [poweringOn, setPoweringOn] = useState<number | null>(null);
   const queryClient = useQueryClient();
 
   const { data: printers, isLoading } = useQuery({
@@ -722,6 +793,12 @@ export function PrintersPage() {
     queryFn: api.getPrinters,
   });
 
+  // Fetch all smart plugs to know which printers have them
+  const { data: smartPlugs } = useQuery({
+    queryKey: ['smart-plugs'],
+    queryFn: api.getSmartPlugs,
+  });
+
   // Fetch maintenance overview for all printers to show badges
   const { data: maintenanceOverview } = useQuery({
     queryKey: ['maintenanceOverview'],
@@ -742,6 +819,17 @@ export function PrintersPage() {
     {} as Record<number, PrinterMaintenanceInfo>
   ) || {};
 
+  // Create a map of printer_id -> smart plug
+  const smartPlugByPrinter = smartPlugs?.reduce(
+    (acc, plug) => {
+      if (plug.printer_id) {
+        acc[plug.printer_id] = plug;
+      }
+      return acc;
+    },
+    {} as Record<number, typeof smartPlugs[0]>
+  ) || {};
+
   const addMutation = useMutation({
     mutationFn: api.createPrinter,
     onSuccess: () => {
@@ -750,6 +838,17 @@ export function PrintersPage() {
     },
   });
 
+  const powerOnMutation = useMutation({
+    mutationFn: (plugId: number) => api.controlSmartPlug(plugId, 'on'),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
+      setPoweringOn(null);
+    },
+    onError: () => {
+      setPoweringOn(null);
+    },
+  });
+
   const toggleHideDisconnected = () => {
     const newValue = !hideDisconnected;
     setHideDisconnected(newValue);
@@ -773,6 +872,50 @@ export function PrintersPage() {
             />
             Hide offline
           </label>
+          {/* Power dropdown for offline printers with smart plugs */}
+          {hideDisconnected && Object.keys(smartPlugByPrinter).length > 0 && (
+            <div className="relative">
+              <button
+                onClick={() => setShowPowerDropdown(!showPowerDropdown)}
+                className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-white dark:bg-bambu-dark-secondary border border-gray-200 dark:border-bambu-dark-tertiary rounded-lg text-gray-600 dark:text-bambu-gray hover:text-gray-900 dark:hover:text-white hover:border-bambu-green transition-colors"
+              >
+                <Power className="w-4 h-4" />
+                Power On
+                <ChevronDown className={`w-3 h-3 transition-transform ${showPowerDropdown ? 'rotate-180' : ''}`} />
+              </button>
+              {showPowerDropdown && (
+                <>
+                  {/* Backdrop to close dropdown */}
+                  <div
+                    className="fixed inset-0 z-10"
+                    onClick={() => setShowPowerDropdown(false)}
+                  />
+                  <div className="absolute right-0 mt-2 w-56 bg-white dark:bg-bambu-dark-secondary border border-gray-200 dark:border-bambu-dark-tertiary rounded-lg shadow-lg z-20 py-1">
+                    <div className="px-3 py-2 text-xs text-gray-500 dark:text-bambu-gray border-b border-gray-200 dark:border-bambu-dark-tertiary">
+                      Offline printers with smart plugs
+                    </div>
+                    {printers?.filter(p => smartPlugByPrinter[p.id]).map(printer => (
+                      <PowerDropdownItem
+                        key={printer.id}
+                        printer={printer}
+                        plug={smartPlugByPrinter[printer.id]}
+                        onPowerOn={(plugId) => {
+                          setPoweringOn(plugId);
+                          powerOnMutation.mutate(plugId);
+                        }}
+                        isPowering={poweringOn === smartPlugByPrinter[printer.id]?.id}
+                      />
+                    ))}
+                    {printers?.filter(p => smartPlugByPrinter[p.id]).length === 0 && (
+                      <div className="px-3 py-2 text-sm text-bambu-gray">
+                        No printers with smart plugs
+                      </div>
+                    )}
+                  </div>
+                </>
+              )}
+            </div>
+          )}
           <Button onClick={() => setShowAddModal(true)}>
             <Plus className="w-4 h-4" />
             Add Printer

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


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


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


+ 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="16x16" href="/img/favicon-16x16.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
-    <script type="module" crossorigin src="/assets/index-DRrbGWbR.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-CVJMI5JU.css">
+    <script type="module" crossorigin src="/assets/index-zCm77ErN.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-Cx4S-qVJ.css">
   </head>
   <body>
     <div id="root"></div>

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