Added power switch dropdown to printer card for offline printers

This commit is contained in:
maziggy
2025-12-07 10:42:41 +00:00
parent 06bfaa3c74
commit 29327d0f16
5 changed files with 545 additions and 402 deletions
+143
View File
@@ -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 one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -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>