mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
sensor stream throttle and new roster panel
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import RoverRoster from './RoverRoster.jsx';
|
||||
|
||||
const MODES = [
|
||||
{ key: 'open', label: 'Open' },
|
||||
@@ -69,32 +70,23 @@ export default function AdminPanel() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5 text-sm">
|
||||
{roster.map((rover) => (
|
||||
<div key={rover.id} className="surface flex flex-wrap items-center justify-between gap-0.5">
|
||||
<div>
|
||||
<p className="text-slate-100">{rover.name}</p>
|
||||
<p className="text-xs text-slate-400">{lockMap[rover.id] ? 'locked' : 'unlocked'}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-0.5 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleLockToggle(rover.id, !lockMap[rover.id])}
|
||||
className="button-dark"
|
||||
>
|
||||
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleForceControl(rover.id)}
|
||||
className="button-dark"
|
||||
>
|
||||
Force
|
||||
</button>
|
||||
</div>
|
||||
<RoverRoster
|
||||
roster={roster}
|
||||
renderActions={(rover) => (
|
||||
<div className="flex flex-wrap gap-0.5 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleLockToggle(rover.id, !lockMap[rover.id])}
|
||||
className="button-dark"
|
||||
>
|
||||
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
|
||||
</button>
|
||||
<button type="button" onClick={() => handleForceControl(rover.id)} className="button-dark">
|
||||
Force
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
function classNames(...values) {
|
||||
return values.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function formatBattery(rover) {
|
||||
const percent = rover?.batteryState?.percentDisplay;
|
||||
if (percent == null) return '--';
|
||||
return `${percent}%`;
|
||||
}
|
||||
|
||||
function batteryClass(rover) {
|
||||
if (!rover?.batteryState) return 'text-slate-400';
|
||||
if (rover.batteryState.urgentActive) return 'text-red-400';
|
||||
if (rover.batteryState.warnActive) return 'text-amber-300';
|
||||
return 'text-emerald-300';
|
||||
}
|
||||
|
||||
export default function RoverRoster({
|
||||
title,
|
||||
roster = [],
|
||||
emptyText = 'No roster data.',
|
||||
renderActions = null,
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-0.5 text-sm">
|
||||
{title && <p className="text-sm text-slate-400">{title}</p>}
|
||||
{roster.length === 0 ? (
|
||||
<p className="text-sm text-slate-500">{emptyText}</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5 text-sm">
|
||||
{roster.map((rover) => (
|
||||
<li
|
||||
key={rover.id}
|
||||
className={classNames(
|
||||
'surface flex flex-wrap items-center justify-between gap-0.5',
|
||||
rover.locked && 'bg-red-900/40',
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<p className="text-slate-200">{rover.name}</p>
|
||||
<p className="text-xs text-slate-500 flex flex-wrap items-center gap-1">
|
||||
<span>{rover.locked ? 'locked' : 'free'}</span>
|
||||
{rover.lockReason && <span className="rounded bg-black/30 px-1">{rover.lockReason}</span>}
|
||||
<span className={classNames('font-semibold', batteryClass(rover))}>
|
||||
Battery {formatBattery(rover)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{renderActions ? renderActions(rover) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
|
||||
import RoverRoster from './RoverRoster.jsx';
|
||||
|
||||
function formatMetric(value, fallback = '--') {
|
||||
if (value == null || value === '') return fallback;
|
||||
@@ -97,37 +98,22 @@ export default function TelemetryPanel() {
|
||||
{rawSnippet}
|
||||
</pre>
|
||||
)}
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-slate-400">Rovers</p>
|
||||
{roster.length === 0 ? (
|
||||
// if there ARE NO rovers
|
||||
<p className="text-sm text-slate-500">No roster data.</p>
|
||||
) : (
|
||||
// if there ARE rovers
|
||||
<ul className="space-y-0.5 text-sm">
|
||||
{roster.map((rover) => (
|
||||
<li key={rover.id} className="surface flex items-center justify-between gap-0.5">
|
||||
<div>
|
||||
<p className="text-slate-200">{rover.name}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{rover.locked ? 'locked' : 'free'} · {rover.lastSeen ? 'seen' : 'unknown'}
|
||||
</p>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRequest(rover.id)}
|
||||
disabled={pending[rover.id]}
|
||||
className="button-dark disabled:opacity-40"
|
||||
>
|
||||
{pending[rover.id] ? '...' : 'request'}
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<RoverRoster
|
||||
title="Rovers"
|
||||
roster={roster}
|
||||
renderActions={(rover) =>
|
||||
isAdmin ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRequest(rover.id)}
|
||||
disabled={pending[rover.id]}
|
||||
className="button-dark disabled:opacity-40"
|
||||
>
|
||||
{pending[rover.id] ? '...' : 'request'}
|
||||
</button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user