refactor: Decentralize data fetching and add axis initialization
Refactor data fetching architecture from centralized App state to component-local data management for improved maintainability and data freshness guarantees. Changes: - SettingsModal: Fetch config data on modal open - RecipePanel: Fetch recipe list on panel open - IOMonitorPage: Fetch IO list on page mount with real-time updates - Remove unnecessary props drilling through component hierarchy - Simplify App.tsx by removing centralized config/recipes state New feature: - Add InitializeModal for sequential axis initialization (X, Y, Z) - Each axis initializes with 3-second staggered start - Progress bar animation for each axis - Auto-close on completion 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,51 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RotateCw } from 'lucide-react';
|
||||
import { IOPoint } from '../types';
|
||||
import { comms } from '../communication';
|
||||
|
||||
interface IOMonitorPageProps {
|
||||
ioPoints: IOPoint[];
|
||||
onToggle: (id: number, type: 'input' | 'output') => void;
|
||||
activeIOTab: 'in' | 'out';
|
||||
onIOTabChange: (tab: 'in' | 'out') => void;
|
||||
}
|
||||
|
||||
export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ ioPoints, onToggle, activeIOTab, onIOTabChange }) => {
|
||||
export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
|
||||
const [ioPoints, setIoPoints] = useState<IOPoint[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [activeIOTab, setActiveIOTab] = useState<'in' | 'out'>('in');
|
||||
|
||||
// Fetch initial IO list when page mounts
|
||||
useEffect(() => {
|
||||
const fetchIOList = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const ioStr = await comms.getIOList();
|
||||
const ioData: IOPoint[] = JSON.parse(ioStr);
|
||||
setIoPoints(ioData);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch IO list:', e);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
fetchIOList();
|
||||
|
||||
// Subscribe to real-time IO updates
|
||||
const unsubscribe = comms.subscribe((msg: any) => {
|
||||
if (msg?.type === 'STATUS_UPDATE' && msg.ioState) {
|
||||
setIoPoints(prev => {
|
||||
const newIO = [...prev];
|
||||
msg.ioState.forEach((update: { id: number, type: string, state: boolean }) => {
|
||||
const idx = newIO.findIndex(p => p.id === update.id && p.type === update.type);
|
||||
if (idx >= 0) newIO[idx] = { ...newIO[idx], state: update.state };
|
||||
});
|
||||
return newIO;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const points = ioPoints.filter(p => p.type === (activeIOTab === 'in' ? 'input' : 'output'));
|
||||
|
||||
return (
|
||||
@@ -29,13 +66,13 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ ioPoints, onToggle
|
||||
|
||||
<div className="bg-black/40 backdrop-blur-md p-1 rounded-full border border-white/10 flex gap-1">
|
||||
<button
|
||||
onClick={() => onIOTabChange('in')}
|
||||
onClick={() => setActiveIOTab('in')}
|
||||
className={`px-6 py-2 rounded-full font-tech font-bold text-sm transition-all ${activeIOTab === 'in' ? 'bg-neon-yellow/20 text-neon-yellow border border-neon-yellow shadow-[0_0_15px_rgba(255,230,0,0.3)]' : 'text-slate-400 hover:text-white hover:bg-white/5'}`}
|
||||
>
|
||||
INPUTS ({ioPoints.filter(p => p.type === 'input').length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onIOTabChange('out')}
|
||||
onClick={() => setActiveIOTab('out')}
|
||||
className={`px-6 py-2 rounded-full font-tech font-bold text-sm transition-all ${activeIOTab === 'out' ? 'bg-neon-green/20 text-neon-green border border-neon-green shadow-[0_0_15px_rgba(10,255,0,0.3)]' : 'text-slate-400 hover:text-white hover:bg-white/5'}`}
|
||||
>
|
||||
OUTPUTS ({ioPoints.filter(p => p.type === 'output').length})
|
||||
@@ -44,38 +81,43 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ ioPoints, onToggle
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-950/40 backdrop-blur-md flex-1 overflow-y-auto custom-scrollbar rounded-lg border border-white/5">
|
||||
{/* Grid Layout - More columns for full screen */}
|
||||
{/* Grid Layout - 2 Columns for list view */}
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-2 p-4">
|
||||
{points.map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
onClick={() => onToggle(p.id, p.type)}
|
||||
className={`
|
||||
flex items-center gap-4 px-4 py-3 cursor-pointer transition-all border
|
||||
clip-tech-sm group hover:translate-x-1
|
||||
${p.state
|
||||
? (p.type === 'output'
|
||||
? 'bg-neon-green/10 border-neon-green text-neon-green shadow-[0_0_15px_rgba(10,255,0,0.2)]'
|
||||
: 'bg-neon-yellow/10 border-neon-yellow text-neon-yellow shadow-[0_0_15px_rgba(255,230,0,0.2)]')
|
||||
: 'bg-slate-900/40 border-slate-800 text-slate-500 hover:border-slate-600 hover:bg-slate-800/40'}
|
||||
`}
|
||||
>
|
||||
{/* LED Indicator */}
|
||||
<div className={`w-3 h-3 rounded-full shrink-0 transition-all ${p.state ? (p.type === 'output' ? 'bg-neon-green shadow-[0_0_8px_#0aff00]' : 'bg-neon-yellow shadow-[0_0_8px_#ffe600]') : 'bg-slate-800 border border-slate-700'}`}></div>
|
||||
{isLoading ? (
|
||||
<div className="h-full flex flex-col items-center justify-center gap-4 animate-pulse">
|
||||
<RotateCw className="w-16 h-16 text-neon-blue animate-spin" />
|
||||
<div className="text-xl font-tech text-neon-blue tracking-widest">LOADING IO POINTS...</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-2 p-4">
|
||||
{points.map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
onClick={() => onToggle(p.id, p.type)}
|
||||
className={`
|
||||
flex items-center gap-4 px-4 py-3 cursor-pointer transition-all border
|
||||
clip-tech-sm group hover:translate-x-1
|
||||
${p.state
|
||||
? (p.type === 'output'
|
||||
? 'bg-neon-green/10 border-neon-green text-neon-green shadow-[0_0_15px_rgba(10,255,0,0.2)]'
|
||||
: 'bg-neon-yellow/10 border-neon-yellow text-neon-yellow shadow-[0_0_15px_rgba(255,230,0,0.2)]')
|
||||
: 'bg-slate-900/40 border-slate-800 text-slate-500 hover:border-slate-600 hover:bg-slate-800/40'}
|
||||
`}
|
||||
>
|
||||
{/* LED Indicator */}
|
||||
<div className={`w-3 h-3 rounded-full shrink-0 transition-all ${p.state ? (p.type === 'output' ? 'bg-neon-green shadow-[0_0_8px_#0aff00]' : 'bg-neon-yellow shadow-[0_0_8px_#ffe600]') : 'bg-slate-800 border border-slate-700'}`}></div>
|
||||
|
||||
{/* ID Badge */}
|
||||
<div className={`w-12 font-mono font-bold text-lg ${p.state ? 'text-white' : 'text-slate-600'}`}>
|
||||
{p.type === 'input' ? 'I' : 'Q'}{p.id.toString().padStart(2, '0')}
|
||||
</div>
|
||||
{/* ID Badge */}
|
||||
<div className={`w-12 font-mono font-bold text-lg ${p.state ? 'text-white' : 'text-slate-600'}`}>
|
||||
{p.type === 'input' ? 'I' : 'Q'}{p.id.toString().padStart(2, '0')}
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div className={`flex-1 font-bold uppercase tracking-wide truncate ${p.state ? 'text-white' : 'text-slate-500'} group-hover:text-white transition-colors`}>
|
||||
{p.name}
|
||||
{/* Name */}
|
||||
<div className={`flex-1 font-bold uppercase tracking-wide truncate ${p.state ? 'text-white' : 'text-slate-500'} group-hover:text-white transition-colors`}>
|
||||
{p.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user