mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 18:53:06 +00:00
662 lines
20 KiB
JavaScript
662 lines
20 KiB
JavaScript
import { Fragment } from 'inferno';
|
|
import { useBackend } from '../backend';
|
|
import { Box, Button, Flex, Icon, LabeledList, ProgressBar, Section } from '../components';
|
|
import { Window } from '../layouts';
|
|
|
|
import { createLogger } from '../logging';
|
|
const logger = createLogger('fuck');
|
|
|
|
// This UI uses an internal routing system for the many different variants of
|
|
// embedded controllers in use.
|
|
let primaryRoutes = {};
|
|
|
|
/**
|
|
* This is an all-in-one replacement for the following NanoUI Templates:
|
|
* - advanced_airlock_console.tmpl
|
|
* - docking_airlock_console.tmpl
|
|
* - door_access_console.tmpl
|
|
* - escape_pod_console.tmpl
|
|
* - escape_pod_berth_console.tmpl
|
|
* - multi_docking_console.tmpl
|
|
* - phoron_airlock_console.tmpl
|
|
* - simple_airlock_console.tmpl
|
|
* - simple_docking_console.tmpl
|
|
* - simple_docking_console_pod.tmpl -- Funny enough, wasn't used anywhere.
|
|
*/
|
|
|
|
/**
|
|
* Let's cover all of the attributes of `data` for this UI right here.
|
|
* For those unfamiliar with JSDoc syntax, [param] indicates
|
|
* an optional parameter.
|
|
*/
|
|
|
|
/**
|
|
* Interior/Exterior Door Status
|
|
* @typedef {Object} doorStatus
|
|
* @property {('open'|'closed')} state
|
|
* @property {('locked'|'unlocked')} lock
|
|
*/
|
|
|
|
/**
|
|
* Dock Status
|
|
* @typedef {('undocked'|'undocking'|'docking'|'docked')} dockStatus
|
|
*/
|
|
|
|
/**
|
|
* All possible data attributes.
|
|
* @typedef {Object} Data
|
|
* @property {string} internalTemplateName - <Template> to use.
|
|
* @property {number} chamber_pressure - The current pressure of the airlock.
|
|
* @property {boolean} processing - Whether or not the airlock is currently
|
|
* cycling.
|
|
* @property {number} [external_pressure] - Pressure on the "external" side.
|
|
* @property {number} [internal_pressure] - Pressure on the "internal" side.
|
|
* @property {boolean} [purge] - Airlock currently purging?
|
|
* @property {boolean} [secure] - Airlock doors locked?
|
|
* @property {doorStatus} [exterior_status] - Describes the status of the
|
|
* exterior-side door.
|
|
* @property {doorStatus} [interior_status] - Describes the status of the
|
|
* interior-side door.
|
|
*
|
|
* @property {dockStatus} [docking_status] - Used exclusivly for "Docking" type
|
|
* controllers, describes the state of the dock.
|
|
* @property {boolean} [airlock_disabled] - Airlock disabled?
|
|
* @property {boolean} [override_enabled] - Forces the shuttle to undock.
|
|
* @property {string} [docking_codes] - The secret codes to dock a shuttle here
|
|
* @property {string} [name] - Name of the dock.
|
|
*/
|
|
|
|
/**
|
|
* Entrypoint of the UI. This handles finding the correct route to use.
|
|
*/
|
|
export const EmbeddedController = (props, context) => {
|
|
const { act, data } = useBackend(context);
|
|
const { internalTemplateName } = data;
|
|
|
|
const Component = primaryRoutes[internalTemplateName];
|
|
if (!Component) {
|
|
throw Error('Unable to find Component for template name: ' + internalTemplateName);
|
|
}
|
|
|
|
return (
|
|
<Window width={450} height={340} resizable>
|
|
<Window.Content>
|
|
<Component />
|
|
</Window.Content>
|
|
</Window>
|
|
);
|
|
};
|
|
|
|
/** ***************************************************************************\
|
|
* HELPER COMPONENTS *
|
|
\******************************************************************************/
|
|
|
|
/**
|
|
* @typedef {Object} BarProp
|
|
* @property {number} minValue - Minimum value of the bar.
|
|
* @property {number} maxValue - Maximum value of the bar.
|
|
* @property {number} value - Current value between min/max.
|
|
* @property {string} label - Label next to the bar.
|
|
* @property {string} textValue - Value in text.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} StatusDisplayProps
|
|
* @property {array[BarProp]} bars - The bars to display.
|
|
*/
|
|
|
|
/**
|
|
* Used for the upper status display that is used on 90% of these UIs.
|
|
* @param {StatusDisplayProps} props
|
|
*/
|
|
const StatusDisplay = (props, context) => {
|
|
const { bars } = props;
|
|
|
|
return (
|
|
<Section title="Status">
|
|
<LabeledList>
|
|
{bars.map((bar) => (
|
|
<LabeledList.Item key={bar.label} label={bar.label}>
|
|
<ProgressBar color={bar.color(bar.value)} minValue={bar.minValue} maxValue={bar.maxValue} value={bar.value}>
|
|
{bar.textValue}
|
|
</ProgressBar>
|
|
</LabeledList.Item>
|
|
))}
|
|
</LabeledList>
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* This is just a quick helper for most airlock controllers. They usually all
|
|
* have the "Cycle out, cycle in, force out, force in" buttons, so we just have
|
|
* a single component that adjusts for the mild data structure differences
|
|
* on it's own.
|
|
*/
|
|
const StandardControls = (props, context) => {
|
|
const { data, act } = useBackend(context);
|
|
|
|
let externalForceSafe = true;
|
|
if (data['interior_status'] && data.interior_status.state === 'open') {
|
|
externalForceSafe = false;
|
|
} else if (data['external_pressure'] && data['chamber_pressure']) {
|
|
externalForceSafe = !(Math.abs(data['external_pressure'] - data['chamber_pressure']) > 5);
|
|
}
|
|
|
|
let internalForceSafe = true;
|
|
if (data['exterior_status'] && data.exterior_status.state === 'open') {
|
|
internalForceSafe = false;
|
|
} else if (data['internal_pressure'] && data['chamber_pressure']) {
|
|
internalForceSafe = !(Math.abs(data['internal_pressure'] - data['chamber_pressure']) > 5);
|
|
}
|
|
|
|
return (
|
|
<Fragment>
|
|
<Box>
|
|
<Button
|
|
disabled={data.airlock_disabled}
|
|
icon="arrow-left"
|
|
content="Cycle to Exterior"
|
|
onClick={() => act('cycle_ext')}
|
|
/>
|
|
<Button
|
|
disabled={data.airlock_disabled}
|
|
icon="arrow-right"
|
|
content="Cycle to Interior"
|
|
onClick={() => act('cycle_int')}
|
|
/>
|
|
</Box>
|
|
<Box>
|
|
<Button.Confirm
|
|
disabled={data.airlock_disabled}
|
|
color={externalForceSafe ? '' : 'bad'}
|
|
icon="exclamation-triangle"
|
|
confirmIcon="exclamation-triangle"
|
|
content="Force Exterior Door"
|
|
onClick={() => act('force_ext')}
|
|
/>
|
|
<Button.Confirm
|
|
disabled={data.airlock_disabled}
|
|
color={internalForceSafe ? '' : 'bad'}
|
|
icon="exclamation-triangle"
|
|
confirmIcon="exclamation-triangle"
|
|
content="Force Interior Door"
|
|
onClick={() => act('force_int')}
|
|
/>
|
|
</Box>
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* This is a shared component between the EscapePodConsole
|
|
* and the EscapePodBerthConsole. They previously had different data structures
|
|
* but I got rid of that stupid shit.
|
|
*/
|
|
const EscapePodStatus = (props, context) => {
|
|
const { data, act } = useBackend(context);
|
|
|
|
const statusToHtml = {
|
|
'docked': <Armed />,
|
|
'undocking': <Box color="average">EJECTING-STAND CLEAR!</Box>,
|
|
'undocked': <Box color="grey">POD EJECTED</Box>,
|
|
'docking': <Box color="good">INITIALIZING...</Box>,
|
|
};
|
|
|
|
let dockHatch = <Box color="bad">ERROR</Box>;
|
|
|
|
if (data.exterior_status.state === 'open') {
|
|
dockHatch = <Box color="average">OPEN</Box>;
|
|
} else if (data.exterior_status.lock === 'unlocked') {
|
|
dockHatch = <Box color="average">UNSECURED</Box>;
|
|
} else if (data.exterior_status.lock === 'locked') {
|
|
dockHatch = <Box color="good">SECURED</Box>;
|
|
}
|
|
|
|
return (
|
|
<Section>
|
|
<LabeledList>
|
|
<LabeledList.Item label="Escape Pod Status">{statusToHtml[data.docking_status]}</LabeledList.Item>
|
|
<LabeledList.Item label="Docking Hatch">{dockHatch}</LabeledList.Item>
|
|
</LabeledList>
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Sub-subcomponent for escape pods.
|
|
* Just shows "ARMED" or "SYSTEMS OK" depending on armed status.
|
|
* Keeps me from having to write like, two lines of code.
|
|
*/
|
|
const Armed = (props, context) => {
|
|
const { data, act } = useBackend(context);
|
|
return data.armed ? <Box color="average">ARMED</Box> : <Box color="good">SYSTEMS OK</Box>;
|
|
};
|
|
|
|
/**
|
|
* Shared controls between the berth and the pod itself.
|
|
* Basically just external door control.
|
|
*/
|
|
const EscapePodControls = (props, context) => {
|
|
const { data, act } = useBackend(context);
|
|
|
|
return (
|
|
<Box>
|
|
<Button
|
|
disabled={!data.override_enabled}
|
|
icon="exclamation-triangle"
|
|
content="Force Exterior Door"
|
|
color={data.docking_status !== 'docked' ? 'bad' : ''}
|
|
onClick={() => act('force_door')}
|
|
/>
|
|
<Button
|
|
selected={data.override_enabled}
|
|
color={data.docking_status !== 'docked' ? 'bad' : 'average'}
|
|
icon="exclamation-triangle"
|
|
content="Override"
|
|
onClick={() => act('toggle_override')}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Just a neat little helper for all the different states of dock.
|
|
*/
|
|
const DockStatus = (props, context) => {
|
|
const { data, act } = useBackend(context);
|
|
|
|
const statusToHtml = {
|
|
'docked': <Box color="good">DOCKED</Box>,
|
|
'docking': <Box color="average">DOCKING</Box>,
|
|
'undocking': <Box color="average">UNDOCKING</Box>,
|
|
'undocked': <Box color="grey">NOT IN USE</Box>,
|
|
};
|
|
|
|
let dockStatus = statusToHtml[data.docking_status];
|
|
|
|
if (data.override_enabled) {
|
|
dockStatus = <Box color="bad">{data.docking_status.toUpperCase()}-OVERRIDE ENABLED</Box>;
|
|
}
|
|
|
|
return dockStatus;
|
|
};
|
|
|
|
/** ***************************************************************************\
|
|
* ROUTES *
|
|
\******************************************************************************/
|
|
|
|
/**
|
|
* Advanced airlock consoles display the external pressure,
|
|
* the internal pressure, and the chamber pressure separately.
|
|
* They also have a PURGE and SECURE option for safety.
|
|
* Replaces advanced_airlock_console.tmpl
|
|
*/
|
|
const AirlockConsoleAdvanced = (props, context) => {
|
|
const { act, data } = useBackend(context);
|
|
|
|
const color = (value) => {
|
|
return value < 80 || value > 120 ? 'bad' : value < 95 || value > 110 ? 'average' : 'good';
|
|
};
|
|
|
|
const bars = [
|
|
{
|
|
minValue: 0,
|
|
maxValue: 202,
|
|
value: data.external_pressure,
|
|
label: 'External Pressure',
|
|
textValue: data.external_pressure + ' kPa',
|
|
color: color,
|
|
},
|
|
{
|
|
minValue: 0,
|
|
maxValue: 202,
|
|
value: data.chamber_pressure,
|
|
label: 'Chamber Pressure',
|
|
textValue: data.chamber_pressure + ' kPa',
|
|
color: color,
|
|
},
|
|
{
|
|
minValue: 0,
|
|
maxValue: 202,
|
|
value: data.internal_pressure,
|
|
label: 'Internal Pressure',
|
|
textValue: data.internal_pressure + ' kPa',
|
|
color: color,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Fragment>
|
|
<StatusDisplay bars={bars} />
|
|
<Section title="Controls">
|
|
<StandardControls />
|
|
<Box>
|
|
<Button icon="sync" content="Purge" onClick={() => act('purge')} />
|
|
<Button icon="lock-open" content="Secure" onClick={() => act('secure')} />
|
|
</Box>
|
|
<Box>
|
|
<Button disabled={!data.processing} icon="ban" color="bad" content="Abort" onClick={() => act('abort')} />
|
|
</Box>
|
|
</Section>
|
|
</Fragment>
|
|
);
|
|
};
|
|
primaryRoutes['AirlockConsoleAdvanced'] = AirlockConsoleAdvanced;
|
|
|
|
/**
|
|
* Simple airlock consoles are the least complicated airlock controller.
|
|
* They show the current chamber pressure, two cycle buttons, and two
|
|
* force door buttons. That's it.
|
|
* Replaces simple_airlock_console.tmpl
|
|
*/
|
|
const AirlockConsoleSimple = (props, context) => {
|
|
const { act, data } = useBackend(context);
|
|
|
|
const bars = [
|
|
{
|
|
minValue: 0,
|
|
maxValue: 202,
|
|
value: data.chamber_pressure,
|
|
label: 'Chamber Pressure',
|
|
textValue: data.chamber_pressure + ' kPa',
|
|
color: (value) => {
|
|
return value < 80 || value > 120 ? 'bad' : value < 95 || value > 110 ? 'average' : 'good';
|
|
},
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Fragment>
|
|
<StatusDisplay bars={bars} />
|
|
<Section title="Controls">
|
|
<StandardControls />
|
|
<Box>
|
|
<Button disabled={!data.processing} icon="ban" color="bad" content="Abort" onClick={() => act('abort')} />
|
|
</Box>
|
|
</Section>
|
|
</Fragment>
|
|
);
|
|
};
|
|
primaryRoutes['AirlockConsoleSimple'] = AirlockConsoleSimple;
|
|
|
|
/**
|
|
* Phoron airlock consoles don't actually cycle *pressure*, they cycle
|
|
* phoron, for use on transitioning to the outside environment of a phoron
|
|
* atmosphere planet.
|
|
* Replaces phoron_airlock_console.tmpl
|
|
*/
|
|
const AirlockConsolePhoron = (props, context) => {
|
|
const { act, data } = useBackend(context);
|
|
|
|
const bars = [
|
|
{
|
|
minValue: 0,
|
|
maxValue: 202,
|
|
value: data.chamber_pressure,
|
|
label: 'Chamber Pressure',
|
|
textValue: data.chamber_pressure + ' kPa',
|
|
color: (value) => {
|
|
return value < 80 || value > 120 ? 'bad' : value < 95 || value > 110 ? 'average' : 'good';
|
|
},
|
|
},
|
|
{
|
|
minValue: 0,
|
|
maxValue: 100,
|
|
value: data.chamber_phoron,
|
|
label: 'Chamber Phoron',
|
|
textValue: data.chamber_phoron + ' mol',
|
|
color: (value) => {
|
|
return value > 5 ? 'bad' : value > 0.5 ? 'average' : 'good';
|
|
},
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Fragment>
|
|
<StatusDisplay bars={bars} />
|
|
<Section title="Controls">
|
|
<StandardControls />
|
|
<Box>
|
|
<Button disabled={!data.processing} icon="ban" color="bad" content="Abort" onClick={() => act('abort')} />
|
|
</Box>
|
|
</Section>
|
|
</Fragment>
|
|
);
|
|
};
|
|
primaryRoutes['AirlockConsolePhoron'] = AirlockConsolePhoron;
|
|
|
|
/**
|
|
* This is a mix airlock & docking console. It lets you control the dock status
|
|
* as well as the attached airlock.
|
|
* Replaces docking_airlock_console.tmpl
|
|
*/
|
|
const AirlockConsoleDocking = (props, context) => {
|
|
const { act, data } = useBackend(context);
|
|
|
|
const bars = [
|
|
{
|
|
minValue: 0,
|
|
maxValue: 202,
|
|
value: data.chamber_pressure,
|
|
label: 'Chamber Pressure',
|
|
textValue: data.chamber_pressure + ' kPa',
|
|
color: (value) => {
|
|
return value < 80 || value > 120 ? 'bad' : value < 95 || value > 110 ? 'average' : 'good';
|
|
},
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Fragment>
|
|
<Section
|
|
title="Dock"
|
|
buttons={
|
|
data.airlock_disabled || data.override_enabled ? (
|
|
<Button
|
|
icon="exclamation-triangle"
|
|
color={data.override_enabled ? 'red' : ''}
|
|
content="Override"
|
|
onClick={() => act('toggle_override')}
|
|
/>
|
|
) : null
|
|
}>
|
|
<DockStatus />
|
|
</Section>
|
|
<StatusDisplay bars={bars} />
|
|
<Section title="Controls">
|
|
<StandardControls />
|
|
<Box>
|
|
<Button disabled={!data.processing} icon="ban" color="bad" content="Abort" onClick={() => act('abort')} />
|
|
</Box>
|
|
</Section>
|
|
</Fragment>
|
|
);
|
|
};
|
|
primaryRoutes['AirlockConsoleDocking'] = AirlockConsoleDocking;
|
|
|
|
/**
|
|
* Simple docking consoles do not allow you to cycle the airlock. They can
|
|
* force the doors in an emergency, but there is no facility for cycling.
|
|
* They're primarily just there to display the status of the dock.
|
|
* Replaces simple_docking_console.tmpl
|
|
*/
|
|
const DockingConsoleSimple = (props, context) => {
|
|
const { act, data } = useBackend(context);
|
|
|
|
let dockHatch = <Box color="bad">ERROR</Box>;
|
|
|
|
if (data.exterior_status.state === 'open') {
|
|
dockHatch = <Box color="average">OPEN</Box>;
|
|
} else if (data.exterior_status.lock === 'unlocked') {
|
|
dockHatch = <Box color="average">UNSECURED</Box>;
|
|
} else if (data.exterior_status.lock === 'locked') {
|
|
dockHatch = <Box color="good">SECURED</Box>;
|
|
}
|
|
|
|
return (
|
|
<Section
|
|
title="Status"
|
|
buttons={
|
|
<Fragment>
|
|
<Button
|
|
icon="exclamation-triangle"
|
|
disabled={!data.override_enabled}
|
|
content="Force exterior door"
|
|
onClick={() => act('force_door')}
|
|
/>
|
|
<Button
|
|
icon="exclamation-triangle"
|
|
color={data.override_enabled ? 'red' : ''}
|
|
content="Override"
|
|
onClick={() => act('toggle_override')}
|
|
/>
|
|
</Fragment>
|
|
}>
|
|
<LabeledList>
|
|
<LabeledList.Item label="Dock Status">
|
|
<DockStatus />
|
|
</LabeledList.Item>
|
|
<LabeledList.Item label="Docking Hatch">{dockHatch}</LabeledList.Item>
|
|
</LabeledList>
|
|
</Section>
|
|
);
|
|
};
|
|
primaryRoutes['DockingConsoleSimple'] = DockingConsoleSimple;
|
|
|
|
/**
|
|
* Shockingly, the multi docking console is the simplest docking console.
|
|
* It has no functionality except to display the status of multiple airlocks,
|
|
* for bigger shuttles.
|
|
* Replaces multi_docking_console.tmpl
|
|
*/
|
|
const DockingConsoleMulti = (props, context) => {
|
|
const { data } = useBackend(context);
|
|
return (
|
|
<Fragment>
|
|
<Section title="Docking Status">
|
|
<DockStatus />
|
|
</Section>
|
|
<Section title="Airlocks">
|
|
{data.airlocks.length ? (
|
|
<LabeledList>
|
|
{data.airlocks.map((airlock) => (
|
|
<LabeledList.Item
|
|
color={airlock.override_enabled ? 'bad' : 'good'}
|
|
key={airlock.name}
|
|
label={airlock.name}>
|
|
{airlock.override_enabled ? 'OVERRIDE ENABLED' : 'STATUS OK'}
|
|
</LabeledList.Item>
|
|
))}
|
|
</LabeledList>
|
|
) : (
|
|
<Flex height="100%" mt="0.5em">
|
|
<Flex.Item grow="1" align="center" textAlign="center" color="bad">
|
|
<Icon name="door-closed" mb="0.5rem" size="5" />
|
|
<br />
|
|
No airlocks found.
|
|
</Flex.Item>
|
|
</Flex>
|
|
)}
|
|
</Section>
|
|
</Fragment>
|
|
);
|
|
};
|
|
primaryRoutes['DockingConsoleMulti'] = DockingConsoleMulti;
|
|
|
|
/**
|
|
* Airlock but without anything other than doors. Separates clean rooms.
|
|
* Replaces door_access_console.tmpl
|
|
*/
|
|
const DoorAccessConsole = (props, context) => {
|
|
const { act, data } = useBackend(context);
|
|
|
|
let interiorOpen = data.interior_status.state === 'open' || data.exterior_status.state === 'closed';
|
|
let exteriorOpen = data.exterior_status.state === 'open' || data.interior_status.state === 'closed';
|
|
|
|
return (
|
|
<Section
|
|
title="Status"
|
|
buttons={
|
|
<Fragment>
|
|
{/* Interior Button */}
|
|
<Button
|
|
icon={interiorOpen ? 'arrow-left' : 'exclamation-triangle'}
|
|
content={interiorOpen ? 'Cycle To Exterior' : 'Lock Exterior Door'}
|
|
onClick={() => {
|
|
act(interiorOpen ? 'cycle_ext_door' : 'force_ext');
|
|
}}
|
|
/>
|
|
{/* Exterior Button */}
|
|
<Button
|
|
icon={exteriorOpen ? 'arrow-right' : 'exclamation-triangle'}
|
|
content={exteriorOpen ? 'Cycle To Interior' : 'Lock Interior Door'}
|
|
onClick={() => {
|
|
act(exteriorOpen ? 'cycle_int_door' : 'force_int');
|
|
}}
|
|
/>
|
|
</Fragment>
|
|
}>
|
|
<LabeledList>
|
|
<LabeledList.Item label="Exterior Door Status">
|
|
{data.exterior_status.state === 'closed' ? 'Locked' : 'Open'}
|
|
</LabeledList.Item>
|
|
<LabeledList.Item label="Interior Door Status">
|
|
{data.interior_status.state === 'closed' ? 'Locked' : 'Open'}
|
|
</LabeledList.Item>
|
|
</LabeledList>
|
|
</Section>
|
|
);
|
|
};
|
|
primaryRoutes['DoorAccessConsole'] = DoorAccessConsole;
|
|
|
|
/**
|
|
* These are the least airlock-like UIs here, but they're "close enough".
|
|
* Replaces escape_pod_console.tmpl
|
|
*/
|
|
const EscapePodConsole = (props, context) => {
|
|
const { act, data } = useBackend(context);
|
|
return (
|
|
<Fragment>
|
|
<EscapePodStatus />
|
|
<Section title="Controls">
|
|
<EscapePodControls />
|
|
<Box>
|
|
<Button
|
|
icon="exclamation-triangle"
|
|
disabled={data.armed}
|
|
color={data.armed ? 'bad' : 'average'}
|
|
content="ARM"
|
|
onClick={() => act('manual_arm')}
|
|
/>
|
|
<Button
|
|
icon="exclamation-triangle"
|
|
disabled={!data.can_force}
|
|
color="bad"
|
|
content="MANUAL EJECT"
|
|
onClick={() => act('force_launch')}
|
|
/>
|
|
</Box>
|
|
</Section>
|
|
</Fragment>
|
|
);
|
|
};
|
|
primaryRoutes['EscapePodConsole'] = EscapePodConsole;
|
|
|
|
/**
|
|
* These are the least airlock-like UIs here, but they're "close enough".
|
|
* Replaces escape_pod_berth_console.tmpl
|
|
*/
|
|
const EscapePodBerthConsole = (props, context) => {
|
|
const { data } = useBackend(context);
|
|
return (
|
|
<Fragment>
|
|
<EscapePodStatus />
|
|
<Section title="Controls">
|
|
<EscapePodControls />
|
|
</Section>
|
|
</Fragment>
|
|
);
|
|
};
|
|
primaryRoutes['EscapePodBerthConsole'] = EscapePodBerthConsole;
|