diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index 8362ca2..4dc1090 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -657,11 +657,8 @@ export default function App() { return () => window.clearTimeout(timer); }, [layoutSaveNotice]); - const [fleetCreateRequest, setFleetCreateRequest] = useState(0); const sessionOverview = useSessionOverviewMode(activeDefinition?.kind === "recordings"); - const onAddVehicle = useCallback(() => setFleetCreateRequest(value => value + 1), []); const contentActions = useApplicationPanelActions({ - onAddVehicle, definition: activeDefinition, refreshRuntime: runtime.refresh, resetConnectionScenario: runtime.resetConnectionScenario, @@ -873,7 +870,6 @@ export default function App() { void; refreshRuntime: () => void; resetConnectionScenario?: () => Promise; connectionScenarioResetting: boolean; @@ -45,7 +44,6 @@ export function deviceRuntimeUtilityAction({ export function useApplicationPanelActions({ definition, - onAddVehicle, refreshRuntime, resetConnectionScenario, connectionScenarioResetting, @@ -56,7 +54,6 @@ export function useApplicationPanelActions({ }: ApplicationPanelActionsOptions): ApplicationPanelUtilityAction[] { return useMemo(() => { const actions: ApplicationPanelUtilityAction[] = []; - if (definition?.kind === "vehicles" && onAddVehicle) actions.push({ label: "Добавить аппарат", icon: "plus", onClick: onAddVehicle }); if (definition?.kind === "device") { actions.push(deviceRuntimeUtilityAction({ refreshRuntime, @@ -81,7 +78,6 @@ export function useApplicationPanelActions({ return actions; }, [ definition, - onAddVehicle, refreshRuntime, resetConnectionScenario, connectionScenarioResetting, diff --git a/apps/control-station/src/workspaces/Workspaces.tsx b/apps/control-station/src/workspaces/Workspaces.tsx index 6f79938..2aeadd5 100644 --- a/apps/control-station/src/workspaces/Workspaces.tsx +++ b/apps/control-station/src/workspaces/Workspaces.tsx @@ -431,7 +431,7 @@ export function WorkspaceRenderer(props: WorkspaceRendererProps) { case "missions": return ; case "vehicles": - return ; + return ; case "catalog": return ; case "contour-health": diff --git a/apps/control-station/src/workspaces/contracts.ts b/apps/control-station/src/workspaces/contracts.ts index 01882df..e94a54f 100644 --- a/apps/control-station/src/workspaces/contracts.ts +++ b/apps/control-station/src/workspaces/contracts.ts @@ -37,7 +37,6 @@ export interface LaboratoryViewAction { export interface WorkspaceRendererProps { launchProfile?: WorkspaceLaunchProfile; - fleetCreateRequest?: number; headerToolsHost?: HTMLElement | null; definition: WorkspaceDefinition; state: MissionRuntimeState | null; diff --git a/apps/control-station/src/workspaces/fleet/BoardComputer.tsx b/apps/control-station/src/workspaces/fleet/BoardComputer.tsx new file mode 100644 index 0000000..1539d93 --- /dev/null +++ b/apps/control-station/src/workspaces/fleet/BoardComputer.tsx @@ -0,0 +1,21 @@ +import {Button,Icon,IconButton,ResourceRow,StatusBadge,Window} from '@nodedc/ui-react'; +import type {Vehicle} from '../../core/fleet/useFleet'; + +export const boardStatus=(item:Vehicle,available=true)=>item.enrollment==='pending'?'Подтверждаем привязку':item.enrollment==='revoked'?'Не привязан':item.enrollment==='failed'?'Привязка не завершена':item.connectivity==='online'?(available?'В сети':'Нет свежих данных'):'Нет связи'; +export function BoardComputerRow({vehicle,enabled,onSettings,onMonitor,onAdd,open,onToggle}:{vehicle:Vehicle;enabled:boolean;onSettings:()=>void;onMonitor:()=>void;onAdd?:()=>void;open:boolean;onToggle:()=>void}){ + const label=boardStatus(vehicle,enabled); + return } title="Бортовой компьютер" description={vehicle.host?.hostname||'Mission Core Node'} metadata={label} statusPlacement="leading" + status={} + actions={<>{onAdd&&}}/>; +} +export function BoardComputerSettings({vehicle,enabled,onClose,onRevoke}:{vehicle:Vehicle;enabled:boolean;onClose:()=>void;onRevoke:()=>void}){ + return +
{boardStatus(vehicle,enabled)} + {vehicle.notice&&

{vehicle.notice}

} +
Идентификатор БК
{vehicle.node_id}
Последняя связь
{vehicle.last_seen?new Date(vehicle.last_seen*1000).toLocaleString('ru-RU'):'Соединение ещё не получено'}
+ {vehicle.host&&<>
Имя БК в системе
{vehicle.host.hostname}
Операционная система
{vehicle.host.os}
Архитектура
{vehicle.host.architecture}
Процессоры
{vehicle.host.cpus}
Память
{vehicle.host.memory_kib?`${(vehicle.host.memory_kib/1048576).toFixed(1)} ГиБ`:'Нет сведений'}
} +
+ {vehicle.enrollment!=='revoked'&&} +
+
; +} diff --git a/apps/control-station/src/workspaces/fleet/NodeInvitationForm.tsx b/apps/control-station/src/workspaces/fleet/NodeInvitationForm.tsx new file mode 100644 index 0000000..443f625 --- /dev/null +++ b/apps/control-station/src/workspaces/fleet/NodeInvitationForm.tsx @@ -0,0 +1,40 @@ +import {useId,useState,type FormEvent} from 'react'; +import {Button,Select,SettingsCard,TextAreaField,TextField,StatusBadge} from '@nodedc/ui-react'; +import {fleetRequest,type FleetPreview,type Vehicle} from '../../core/fleet/useFleet'; +import type {SensorEnrollmentProps} from '../../../../../packages/sensor-ui/src/extensions'; + +export const platforms=[{value:'ugv',label:'Наземный (UGV)'},{value:'uav',label:'Воздушный (UAV)'},{value:'stationary',label:'Стационарный'},{value:'other',label:'Другой'}]; +export const platformLabel=(value:string)=>platforms.find(item=>item.value===value)?.label??value; + +/** The invitation admits a computer; an existing apparatus keeps its identity. */ +export function NodeInvitationForm({vehicle,onClose,onComplete,renderWindow}:{vehicle?:Vehicle;onClose:()=>void;onComplete:(vehicle:Vehicle)=>void;renderWindow:SensorEnrollmentProps['renderWindow']}){ + const form=useId(); + const [revision]=useState(vehicle?.revision); + const [code,setCode]=useState(''),[name,setName]=useState(''),[platform,setPlatform]=useState('ugv'); + const [preview,setPreview]=useState(null),[pending,setPending]=useState(false),[error,setError]=useState(''); + async function inspect(event:FormEvent){ + event.preventDefault();if(pending)return;setPending(true);setError(''); + try{const value=await fleetRequest('/preview','POST',{code:code.trim()});setPreview(value);setName(current=>current||value.name);setCode('');} + catch(e){setError(e instanceof Error?e.message:'Не удалось проверить приглашение.');} + finally{setPending(false);} + } + async function attach(){ + if(!preview||pending)return;setPending(true);setError(''); + try{ + const result=vehicle + ?await fleetRequest(`/${encodeURIComponent(vehicle.id)}/computer`,'POST',{preview_id:preview.preview_id,expected_revision:revision}) + :await fleetRequest('','POST',{preview_id:preview.preview_id,name:name.trim(),platform}); + onComplete(result); + }catch(e){setError(e instanceof Error?e.message:'Не удалось подключить БК.');} + finally{setPending(false);} + } + const replacing=!!vehicle&&vehicle.enrollment!=='revoked'; + return renderWindow({busy:pending,actions:<>{preview?:},content: +
+ {!vehicle&&<> undefined} disabled={pending} /> - ({value:value.kind,label:value.wirelessEnrollment!.label}))]} onChange={setSelected}/> @@ -22,5 +23,6 @@ export function WirelessEnrollmentWindow({contributions,transport,onClose,onChan ); + if(Enrollment&&!enabled)return renderWindow({content:}); return Enrollment?:renderWindow({content:null}); }