feat(fleet): edit apparatus name independently of onboard availability

This commit is contained in:
DCCONSTRUCTIONS
2026-09-25 22:28:22 +03:00
parent ee7576b7b4
commit aecae4a986
8 changed files with 147 additions and 6 deletions
@@ -57,5 +57,8 @@ export function useFleet() {
events.onerror = unavailable;
return () => { active = false; events.close(); if (fallback) clearInterval(fallback); };
}, []);
return { items, error, refresh };
const acceptVehicle = useCallback((vehicle: Vehicle) => {
setItems(current => current?.map(item => item.id === vehicle.id ? vehicle : item) ?? null);
}, []);
return { items, error, refresh, acceptVehicle };
}
@@ -3,9 +3,10 @@ import {Button,Icon,IconButton,Inspector,LoadingRegion} from '@nodedc/ui-react';
import type {BoardLayoutStore} from '../../../../../packages/sensor-ui/src/boardLayout';
import type {SensorBoardContent} from '../../../../../packages/sensor-ui/src/SensorWorkspace';
import type {Vehicle} from '../../core/fleet/useFleet';
import {VehicleIdentitySettings} from './VehicleIdentitySettings';
import {BoardComputerRow} from './BoardComputer';
export function VehicleEquipment({vehicle,enabled,layout,content,onAddComputer,onSettings,onMonitor}:{vehicle:Vehicle;enabled:boolean;layout:BoardLayoutStore;content:SensorBoardContent;onAddComputer:()=>void;onSettings:()=>void;onMonitor:()=>void}){
export function VehicleEquipment({vehicle,enabled,layout,content,onAddComputer,onSettings,onMonitor,onUpdated}:{vehicle:Vehicle;enabled:boolean;layout:BoardLayoutStore;content:SensorBoardContent;onAddComputer:()=>void;onSettings:()=>void;onMonitor:()=>void;onUpdated:(vehicle:Vehicle)=>void}){
const state=useSyncExternalStore(layout.subscribe,layout.getSnapshot);
useEffect(()=>{void layout.load();},[layout]);
const computerOpen=state.value.open_sections.includes('computer');
@@ -16,7 +17,7 @@ export function VehicleEquipment({vehicle,enabled,layout,content,onAddComputer,o
{state.error&&<div role="alert"><p>{state.error}</p><Button onClick={()=>void layout.load()}>Повторить</Button></div>}
<LoadingRegion loading={!state.ready&&!state.error} label="Загрузка раскладки аппарата">
<Inspector variant="panel" openSections={state.value.open_sections.filter(id=>id!=='computer')} onOpenSectionsChange={changeSections} sections={[
{id:'settings',label:'Настройки аппарата',icon:<Icon name="sliders"/>,disabled:!state.ready,content:content.settings},
{id:'settings',label:'Настройки аппарата',icon:<Icon name="sliders"/>,disabled:!state.ready,content:<div className="sensor-content"><VehicleIdentitySettings key={vehicle.id} vehicle={vehicle} onUpdated={onUpdated}/>{content.settings}</div>},
{id:'devices',label:'Оборудование',icon:<Icon name="apps"/>,disabled:!state.ready,content:<div className="sensor-content">
<div className="sensor-actions sensor-inventory-toolbar"><span className="sensor-note">Бортовой компьютер и подключённые к нему устройства</span><div className="sensor-actions"><IconButton label="Подключить бортовой компьютер" onClick={onAddComputer}><Icon name="plus"/></IconButton><IconButton label="Обновить оборудование" onClick={()=>void content.refresh()}><Icon name="refresh"/></IconButton></div></div>
<div><BoardComputerRow vehicle={vehicle} enabled={enabled} showingSavedDevices={content.showingSavedDevices} open={computerOpen} onToggle={toggleComputer} onSettings={onSettings} onMonitor={onMonitor} onAdd={content.canAddDevice?content.addDevice:undefined}/>
@@ -0,0 +1,27 @@
import {useEffect,useState} from 'react';
import {Button,SettingsCard,TextField} from '@nodedc/ui-react';
import {fleetRequest,type Vehicle} from '../../core/fleet/useFleet';
export function VehicleIdentitySettings({vehicle,onUpdated}:{vehicle:Vehicle;onUpdated:(vehicle:Vehicle)=>void}){
const [base,setBase]=useState(vehicle),[name,setName]=useState(vehicle.name);
const [saving,setSaving]=useState(false),[message,setMessage]=useState(''),[error,setError]=useState('');
const dirty=name!==base.name;
useEffect(()=>{if(!dirty&&!saving){setBase(vehicle);setName(vehicle.name);}},[vehicle,dirty,saving]);
const reset=()=>{setBase(vehicle);setName(vehicle.name);setError('');setMessage('');};
const save=async()=>{
if(saving||!name.trim()||!dirty)return;
setSaving(true);setError('');setMessage('');
try{
const updated=await fleetRequest<Vehicle>(`/${encodeURIComponent(vehicle.id)}`,'PATCH',{name:name.trim(),expected_revision:base.revision});
onUpdated(updated);setBase(updated);setName(updated.name);setMessage('Название сохранено');
}catch(reason){setError(reason instanceof Error?reason.message:'Не удалось сохранить название.');}
finally{setSaving(false);}
};
return <SettingsCard title="Основные параметры">
<form className="fleet-form" onSubmit={event=>{event.preventDefault();void save();}}>
<TextField label="Название аппарата" value={name} maxLength={80} autoComplete="off" disabled={saving} onChange={event=>{setName(event.target.value);setMessage('');}}/>
<div className="sensor-actions"><Button type="submit" disabled={!dirty||!name.trim()||saving}>{saving?'Сохранение…':'Сохранить'}</Button>{dirty&&<Button type="button" variant="secondary" disabled={saving} onClick={reset}>Отмена</Button>}</div>
{error&&<p role="alert">{error}</p>}{message&&<p role="status">{message}</p>}
</form>
</SettingsCard>;
}
@@ -6,11 +6,11 @@ import {SensorWorkspace} from '../../../../../packages/sensor-ui/src/SensorWorks
import {createFleetSensorTransport} from '../../core/fleet/sensorTransport';
import type {Vehicle} from '../../core/fleet/useFleet';
import {VehicleEquipment} from './VehicleEquipment';
export function VehicleSensors({vehicle,enabled,onAddComputer,onSettings,onMonitor}:{vehicle:Vehicle;enabled:boolean;onAddComputer:()=>void;onSettings:()=>void;onMonitor:()=>void}){
export function VehicleSensors({vehicle,enabled,onAddComputer,onSettings,onMonitor,onUpdated}:{vehicle:Vehicle;enabled:boolean;onAddComputer:()=>void;onSettings:()=>void;onMonitor:()=>void;onUpdated:(vehicle:Vehicle)=>void}){
const vehicleID=vehicle.id;
const {registry}=useDevicePluginHost();
const sensorContributions=registry.sensorContributions;
const layout=useMemo(()=>boardLayout(vehicleID),[vehicleID]);
const transport=useMemo(()=>createFleetSensorTransport(vehicleID),[vehicleID]);
return <SensorWorkspace contributions={sensorContributions} createRerunHost={createIsolatedRerunHost} key={`${vehicleID}:${vehicle.node_id}`} transport={transport} enabled={enabled} readOfflineInventory renderBoard={content=><VehicleEquipment vehicle={vehicle} enabled={enabled} layout={layout} content={content} onAddComputer={onAddComputer} onSettings={onSettings} onMonitor={onMonitor}/>}/>;
return <SensorWorkspace contributions={sensorContributions} createRerunHost={createIsolatedRerunHost} key={`${vehicleID}:${vehicle.node_id}`} transport={transport} enabled={enabled} readOfflineInventory renderBoard={content=><VehicleEquipment vehicle={vehicle} enabled={enabled} layout={layout} content={content} onAddComputer={onAddComputer} onSettings={onSettings} onMonitor={onMonitor} onUpdated={onUpdated}/>}/>;
}
@@ -29,7 +29,7 @@ export function VehiclesWorkspace({headerToolsHost,headerTitleHost}:{headerTools
:<div className="fleet-workspace">
{detail?<>
<WorkspaceBackButton label="К списку аппаратов" onClick={()=>setSelected(null)} host={headerToolsHost}/>
<VehicleSensors vehicle={detail} enabled={enabled} onAddComputer={()=>setAdding(true)} onSettings={()=>setSettingsOpen(true)} onMonitor={()=>setMonitorOpen(true)}/>
<VehicleSensors onUpdated={fleet.acceptVehicle} vehicle={detail} enabled={enabled} onAddComputer={()=>setAdding(true)} onSettings={()=>setSettingsOpen(true)} onMonitor={()=>setMonitorOpen(true)}/>
</>:!fleet.items?<LoadingRegion loading={!fleet.error} label="Получение аппаратов"/>:fleet.items.length===0?<SettingsCard title="Аппаратов пока нет" description="Создайте аппарат и подключите его бортовой компьютер по приглашению Mission Core Node."/>:<ResourceList aria-label="Аппараты">{fleet.items.map(item=><li key={item.id}><ResourceRow icon={<Icon name="apps"/>} title={item.name} description={platformLabel(item.platform)} status={<StatusBadge tone={!fleet.error&&item.enrollment==='paired'&&item.connectivity==='online'?'success':'neutral'}>{fleet.error?'Нет свежих данных':`БК: ${boardStatus(item)}`}</StatusBadge>} actions={<><IconButton label={`Конфигурация: ${item.name}`} onClick={()=>setSelected(item.id)}><Icon name="sliders"/></IconButton><IconButton label={`Центр наблюдения и управления: ${item.name}`} onClick={()=>{setSelected(item.id);setObservationOpen(true);}}><Icon name="eye"/></IconButton></>}/></li>)}</ResourceList>}
</div>;
return <>{headerTitleHost&&createPortal(titleSuffix,headerTitleHost)}{headerAdd}{headerObservation}{content}