Added PDU Component
This commit is contained in:
1 parent
1ec5ab4bf5
commit
71d631efbe
5 files changed
+545
-104
No files matched your search
+469
-100
@@ -31,6 +31,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import type { SelectChangeEvent } from '@mui/material';
|
||||
import {
|
||||
Add,
|
||||
Cable,
|
||||
DarkMode,
|
||||
Delete,
|
||||
@@ -42,6 +43,7 @@ import {
|
||||
Inventory2,
|
||||
LightMode,
|
||||
PictureAsPdf,
|
||||
Power,
|
||||
Router,
|
||||
Save,
|
||||
Security,
|
||||
@@ -60,6 +62,7 @@ import {
|
||||
clampNumber,
|
||||
createDiagram,
|
||||
createEquipment,
|
||||
createId,
|
||||
createRack,
|
||||
findAvailableU,
|
||||
getOccupancy,
|
||||
@@ -81,6 +84,7 @@ const icons: Record<ComponentType, JSX.Element> = {
|
||||
'storage-array': <Storage fontSize="small" />,
|
||||
'storage-switch': <Storage fontSize="small" />,
|
||||
'patch-panel': <Cable fontSize="small" />,
|
||||
pdu: <Power fontSize="small" />,
|
||||
'cable-management': <Cable fontSize="small" />,
|
||||
kvm: <Terminal fontSize="small" />,
|
||||
'kvm-console': <Inventory2 fontSize="small" />,
|
||||
@@ -260,6 +264,29 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (equipment.type === 'pdu' && equipment.pduMount === 'vertical') {
|
||||
commitDiagram((current) => ({
|
||||
...current,
|
||||
racks: current.racks.map((rack) => {
|
||||
if (rack.id === sourceRack.id && rack.id === destinationRack.id) {
|
||||
return rack;
|
||||
}
|
||||
|
||||
if (rack.id === sourceRack.id) {
|
||||
return { ...rack, equipment: rack.equipment.filter((item) => item.id !== equipment.id) };
|
||||
}
|
||||
|
||||
if (rack.id === destinationRack.id) {
|
||||
return { ...rack, equipment: [...rack.equipment, { ...equipment, uStart: 1 }] };
|
||||
}
|
||||
|
||||
return rack;
|
||||
}),
|
||||
}));
|
||||
setSelection({ kind: 'equipment', rackId, equipmentId: equipment.id });
|
||||
return;
|
||||
}
|
||||
|
||||
const nextU = findAvailableU(
|
||||
destinationRack,
|
||||
equipment.sizeU,
|
||||
@@ -313,7 +340,12 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
const highestOccupiedU = Math.max(0, ...rack.equipment.map((item) => item.uStart + item.sizeU - 1));
|
||||
const highestOccupiedU = Math.max(
|
||||
0,
|
||||
...rack.equipment
|
||||
.filter((item) => !(item.type === 'pdu' && item.pduMount === 'vertical'))
|
||||
.map((item) => item.uStart + item.sizeU - 1),
|
||||
);
|
||||
if (highestOccupiedU > sizeU) {
|
||||
setNotice(`Rack size must be at least ${highestOccupiedU}U for the installed equipment.`);
|
||||
return;
|
||||
@@ -343,15 +375,33 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSize = clampNumber(patch.sizeU ?? equipment.sizeU, 1, 24, equipment.sizeU);
|
||||
const nextU = clampNumber(patch.uStart ?? equipment.uStart, 1, rack.sizeU - nextSize + 1, equipment.uStart);
|
||||
const nextMount = patch.pduMount ?? equipment.pduMount;
|
||||
|
||||
if (!isRangeAvailable(rack, nextU, nextSize, equipmentId)) {
|
||||
if (equipment.type === 'pdu' && nextMount === 'vertical') {
|
||||
updateEquipment(rackId, equipmentId, {
|
||||
...patch,
|
||||
pduMount: 'vertical',
|
||||
sizeU: clampNumber(patch.sizeU ?? equipment.sizeU, 1, 3, 1),
|
||||
uStart: 1,
|
||||
pduSide: patch.pduSide ?? equipment.pduSide ?? 'right',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSize = equipment.type === 'pdu' ? 12 : 24;
|
||||
const nextSize = clampNumber(patch.sizeU ?? equipment.sizeU, 1, maxSize, equipment.sizeU);
|
||||
const maxStart = Math.max(1, rack.sizeU - nextSize + 1);
|
||||
const requestedU = clampNumber(patch.uStart ?? equipment.uStart, 1, maxStart, equipment.uStart);
|
||||
const nextU = isRangeAvailable(rack, requestedU, nextSize, equipmentId)
|
||||
? requestedU
|
||||
: findAvailableU(rack, nextSize, requestedU + nextSize - 1, equipmentId);
|
||||
|
||||
if (nextU === null) {
|
||||
setNotice('That size or position overlaps another component.');
|
||||
return;
|
||||
}
|
||||
|
||||
updateEquipment(rackId, equipmentId, { ...patch, sizeU: nextSize, uStart: nextU });
|
||||
updateEquipment(rackId, equipmentId, { ...patch, pduMount: nextMount, sizeU: nextSize, uStart: nextU });
|
||||
};
|
||||
|
||||
const deleteSelection = () => {
|
||||
@@ -466,7 +516,15 @@ export default function App() {
|
||||
item.type === 'patch-panel'
|
||||
? ` | ${item.patchPanelMedium === 'fiberoptic' ? 'Fiber optic' : 'Copper'} | ${item.patchPanelPorts} ports`
|
||||
: '';
|
||||
const line = `U${item.uStart}-${item.uStart + item.sizeU - 1}: ${item.name} | ${item.manufacturer || 'n/a'} ${item.model || ''} | ${item.assetTag || 'no asset tag'}${patchPanelDetails}`;
|
||||
const pduDetails =
|
||||
item.type === 'pdu'
|
||||
? ` | ${item.pduMount === 'vertical' ? `Vertical ${item.pduSide}, ${item.sizeU}U wide` : `Horizontal ${item.sizeU}U`} | In ${item.pduInputVoltage || 'n/a'} | ${item.pduOutputPorts.length} outputs${item.pduManaged ? ` | Managed ${item.pduHostname || 'no host'}` : ''}`
|
||||
: '';
|
||||
const rackPosition =
|
||||
item.type === 'pdu' && item.pduMount === 'vertical'
|
||||
? `${item.pduSide === 'left' ? 'Left' : 'Right'} side`
|
||||
: `U${item.uStart}-${item.uStart + item.sizeU - 1}`;
|
||||
const line = `${rackPosition}: ${item.name} | ${item.manufacturer || 'n/a'} ${item.model || ''} | ${item.assetTag || 'no asset tag'}${patchPanelDetails}${pduDetails}`;
|
||||
pdf.text(line.slice(0, 115), margin + 12, y);
|
||||
y += 14;
|
||||
});
|
||||
@@ -706,6 +764,8 @@ function RackView({
|
||||
}) {
|
||||
const occupancy = getOccupancy(rack);
|
||||
const rackHeight = rack.sizeU * RACK_ROW_HEIGHT;
|
||||
const horizontalEquipment = rack.equipment.filter((item) => !(item.type === 'pdu' && item.pduMount === 'vertical'));
|
||||
const verticalPdus = rack.equipment.filter((item) => item.type === 'pdu' && item.pduMount === 'vertical');
|
||||
|
||||
return (
|
||||
<Paper
|
||||
@@ -739,80 +799,141 @@ function RackView({
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height: rackHeight,
|
||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#101820' : '#f8fafc'),
|
||||
backgroundImage: (theme) =>
|
||||
`linear-gradient(${alpha(theme.palette.text.primary, 0.08)} 1px, transparent 1px)`,
|
||||
backgroundSize: `100% ${RACK_ROW_HEIGHT}px`,
|
||||
borderLeft: 10,
|
||||
borderRight: 10,
|
||||
borderColor: (theme) => (theme.palette.mode === 'dark' ? '#263442' : '#cbd5e1'),
|
||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#0b1118' : '#edf2f7'),
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: rack.sizeU }, (_, index) => rack.sizeU - index).map((u) => (
|
||||
<Typography
|
||||
key={u}
|
||||
variant="caption"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 3,
|
||||
top: (rack.sizeU - u) * RACK_ROW_HEIGHT - 1,
|
||||
color: 'text.disabled',
|
||||
fontSize: 9,
|
||||
lineHeight: `${RACK_ROW_HEIGHT}px`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{u}
|
||||
</Typography>
|
||||
))}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
left: 34,
|
||||
right: 34,
|
||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#101820' : '#f8fafc'),
|
||||
backgroundImage: (theme) =>
|
||||
`linear-gradient(${alpha(theme.palette.text.primary, 0.08)} 1px, transparent 1px)`,
|
||||
backgroundSize: `100% ${RACK_ROW_HEIGHT}px`,
|
||||
borderLeft: 10,
|
||||
borderRight: 10,
|
||||
borderColor: (theme) => (theme.palette.mode === 'dark' ? '#263442' : '#cbd5e1'),
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: rack.sizeU }, (_, index) => rack.sizeU - index).map((u) => (
|
||||
<Typography
|
||||
key={u}
|
||||
variant="caption"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 3,
|
||||
top: (rack.sizeU - u) * RACK_ROW_HEIGHT - 1,
|
||||
color: 'text.disabled',
|
||||
fontSize: 9,
|
||||
lineHeight: `${RACK_ROW_HEIGHT}px`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{u}
|
||||
</Typography>
|
||||
))}
|
||||
|
||||
{[...rack.equipment]
|
||||
.sort((a, b) => b.uStart - a.uStart)
|
||||
.map((item) => {
|
||||
const top = (rack.sizeU - (item.uStart + item.sizeU - 1)) * RACK_ROW_HEIGHT;
|
||||
const selectedItem = selected?.kind === 'equipment' && selected.equipmentId === item.id;
|
||||
return (
|
||||
<Box
|
||||
key={item.id}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
event.stopPropagation();
|
||||
setDragPayload(event, { source: 'equipment', rackId: rack.id, equipmentId: item.id });
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onSelectEquipment(item.id);
|
||||
}}
|
||||
{[...horizontalEquipment]
|
||||
.sort((a, b) => b.uStart - a.uStart)
|
||||
.map((item) => {
|
||||
const top = (rack.sizeU - (item.uStart + item.sizeU - 1)) * RACK_ROW_HEIGHT;
|
||||
const selectedItem = selected?.kind === 'equipment' && selected.equipmentId === item.id;
|
||||
return (
|
||||
<Box
|
||||
key={item.id}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
event.stopPropagation();
|
||||
setDragPayload(event, { source: 'equipment', rackId: rack.id, equipmentId: item.id });
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onSelectEquipment(item.id);
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 24,
|
||||
right: 8,
|
||||
top,
|
||||
height: item.sizeU * RACK_ROW_HEIGHT,
|
||||
minHeight: RACK_ROW_HEIGHT,
|
||||
border: 2,
|
||||
borderColor: selectedItem ? 'secondary.main' : alpha('#000000', 0.16),
|
||||
bgcolor: item.color,
|
||||
color: '#071018',
|
||||
px: 0.75,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1,
|
||||
cursor: 'grab',
|
||||
overflow: 'hidden',
|
||||
boxShadow: selectedItem ? 4 : 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" noWrap sx={{ fontWeight: 900, minWidth: 0 }}>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ flex: '0 0 auto', fontWeight: 800 }}>
|
||||
U{item.uStart}-{item.uStart + item.sizeU - 1}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{verticalPdus.map((item) => {
|
||||
const selectedItem = selected?.kind === 'equipment' && selected.equipmentId === item.id;
|
||||
const width = 12 + item.sizeU * 7;
|
||||
return (
|
||||
<Box
|
||||
key={item.id}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
event.stopPropagation();
|
||||
setDragPayload(event, { source: 'equipment', rackId: rack.id, equipmentId: item.id });
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onSelectEquipment(item.id);
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
width,
|
||||
left: item.pduSide === 'left' ? 3 : 'auto',
|
||||
right: item.pduSide === 'right' ? 3 : 'auto',
|
||||
border: 2,
|
||||
borderColor: selectedItem ? 'secondary.main' : alpha('#000000', 0.24),
|
||||
bgcolor: item.color,
|
||||
color: '#111827',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'grab',
|
||||
boxShadow: selectedItem ? 4 : 1,
|
||||
overflow: 'hidden',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 24,
|
||||
right: 8,
|
||||
top,
|
||||
height: item.sizeU * RACK_ROW_HEIGHT,
|
||||
minHeight: RACK_ROW_HEIGHT,
|
||||
border: 2,
|
||||
borderColor: selectedItem ? 'secondary.main' : alpha('#000000', 0.16),
|
||||
bgcolor: item.color,
|
||||
color: '#071018',
|
||||
px: 0.75,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1,
|
||||
cursor: 'grab',
|
||||
overflow: 'hidden',
|
||||
boxShadow: selectedItem ? 4 : 1,
|
||||
writingMode: 'vertical-rl',
|
||||
transform: 'rotate(180deg)',
|
||||
fontWeight: 900,
|
||||
lineHeight: 1,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" noWrap sx={{ fontWeight: 900, minWidth: 0 }}>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ flex: '0 0 auto', fontWeight: 800 }}>
|
||||
U{item.uStart}-{item.uStart + item.sizeU - 1}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{item.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
@@ -903,32 +1024,114 @@ function PropertiesPanel({
|
||||
fullWidth
|
||||
/>
|
||||
<TextField label="Component" value={getLibraryItem(selectedEquipment.type)?.label || selectedEquipment.type} InputProps={{ readOnly: true }} fullWidth />
|
||||
<Stack direction="row" spacing={1.5}>
|
||||
<TextField
|
||||
label="Size U"
|
||||
type="number"
|
||||
value={selectedEquipment.sizeU}
|
||||
inputProps={{ min: 1, max: 24 }}
|
||||
onChange={(event) =>
|
||||
onEquipmentPlacementChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
sizeU: clampNumber(event.target.value, 1, 24, selectedEquipment.sizeU),
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Start U"
|
||||
type="number"
|
||||
value={selectedEquipment.uStart}
|
||||
inputProps={{ min: 1, max: selectedEquipmentRack.sizeU }}
|
||||
onChange={(event) =>
|
||||
onEquipmentPlacementChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
uStart: clampNumber(event.target.value, 1, selectedEquipmentRack.sizeU, selectedEquipment.uStart),
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</Stack>
|
||||
{selectedEquipment.type === 'pdu' ? (
|
||||
<>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Mounting</InputLabel>
|
||||
<Select
|
||||
value={selectedEquipment.pduMount}
|
||||
label="Mounting"
|
||||
onChange={(event: SelectChangeEvent) =>
|
||||
onEquipmentPlacementChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduMount: event.target.value as Equipment['pduMount'],
|
||||
sizeU:
|
||||
event.target.value === 'vertical'
|
||||
? clampNumber(selectedEquipment.sizeU, 1, 3, 1)
|
||||
: clampNumber(selectedEquipment.sizeU, 1, 12, 1),
|
||||
})
|
||||
}
|
||||
>
|
||||
<MenuItem value="horizontal">Horizontal rack mount</MenuItem>
|
||||
<MenuItem value="vertical">Vertical side mount</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedEquipment.pduMount === 'vertical' ? (
|
||||
<Stack direction="row" spacing={1.5}>
|
||||
<TextField
|
||||
label="Width U"
|
||||
type="number"
|
||||
value={selectedEquipment.sizeU}
|
||||
inputProps={{ min: 1, max: 3 }}
|
||||
onChange={(event) =>
|
||||
onEquipmentPlacementChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
sizeU: clampNumber(event.target.value, 1, 3, selectedEquipment.sizeU),
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Side</InputLabel>
|
||||
<Select
|
||||
value={selectedEquipment.pduSide}
|
||||
label="Side"
|
||||
onChange={(event: SelectChangeEvent) =>
|
||||
onEquipmentPlacementChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduSide: event.target.value as Equipment['pduSide'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<MenuItem value="left">Left</MenuItem>
|
||||
<MenuItem value="right">Right</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack direction="row" spacing={1.5}>
|
||||
<TextField
|
||||
label="Size U"
|
||||
type="number"
|
||||
value={selectedEquipment.sizeU}
|
||||
inputProps={{ min: 1, max: 12 }}
|
||||
onChange={(event) =>
|
||||
onEquipmentPlacementChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
sizeU: clampNumber(event.target.value, 1, 12, selectedEquipment.sizeU),
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Start U"
|
||||
type="number"
|
||||
value={selectedEquipment.uStart}
|
||||
inputProps={{ min: 1, max: selectedEquipmentRack.sizeU }}
|
||||
onChange={(event) =>
|
||||
onEquipmentPlacementChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
uStart: clampNumber(event.target.value, 1, selectedEquipmentRack.sizeU, selectedEquipment.uStart),
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Stack direction="row" spacing={1.5}>
|
||||
<TextField
|
||||
label="Size U"
|
||||
type="number"
|
||||
value={selectedEquipment.sizeU}
|
||||
inputProps={{ min: 1, max: 24 }}
|
||||
onChange={(event) =>
|
||||
onEquipmentPlacementChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
sizeU: clampNumber(event.target.value, 1, 24, selectedEquipment.sizeU),
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Start U"
|
||||
type="number"
|
||||
value={selectedEquipment.uStart}
|
||||
inputProps={{ min: 1, max: selectedEquipmentRack.sizeU }}
|
||||
onChange={(event) =>
|
||||
onEquipmentPlacementChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
uStart: clampNumber(event.target.value, 1, selectedEquipmentRack.sizeU, selectedEquipment.uStart),
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<TextField
|
||||
label="Manufacturer"
|
||||
value={selectedEquipment.manufacturer}
|
||||
@@ -1001,6 +1204,172 @@ function PropertiesPanel({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{selectedEquipment.type === 'pdu' && (
|
||||
<>
|
||||
<Divider />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>
|
||||
Power Distribution
|
||||
</Typography>
|
||||
<TextField
|
||||
label="Input voltage"
|
||||
value={selectedEquipment.pduInputVoltage}
|
||||
onChange={(event) =>
|
||||
onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduInputVoltage: event.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="208V 3-phase, 220V, 120V"
|
||||
fullWidth
|
||||
/>
|
||||
<Stack spacing={1}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 800 }}>
|
||||
Output Ports
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<Add />}
|
||||
onClick={() =>
|
||||
onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduOutputPorts: [
|
||||
...selectedEquipment.pduOutputPorts,
|
||||
{
|
||||
id: createId('pdu-port'),
|
||||
label: `Port ${selectedEquipment.pduOutputPorts.length + 1}`,
|
||||
voltage: 120,
|
||||
amps: 10,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
Add port
|
||||
</Button>
|
||||
</Stack>
|
||||
{selectedEquipment.pduOutputPorts.length === 0 ? (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
No output ports defined.
|
||||
</Typography>
|
||||
) : (
|
||||
selectedEquipment.pduOutputPorts.map((port) => (
|
||||
<Stack key={port.id} direction="row" spacing={1} alignItems="center">
|
||||
<TextField
|
||||
label="Port"
|
||||
value={port.label}
|
||||
size="small"
|
||||
onChange={(event) =>
|
||||
onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduOutputPorts: selectedEquipment.pduOutputPorts.map((item) =>
|
||||
item.id === port.id ? { ...item, label: event.target.value } : item,
|
||||
),
|
||||
})
|
||||
}
|
||||
sx={{ flex: 1.3 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Volts"
|
||||
type="number"
|
||||
value={port.voltage}
|
||||
size="small"
|
||||
inputProps={{ min: 0, max: 1000 }}
|
||||
onChange={(event) =>
|
||||
onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduOutputPorts: selectedEquipment.pduOutputPorts.map((item) =>
|
||||
item.id === port.id ? { ...item, voltage: clampNumber(event.target.value, 0, 1000, port.voltage) } : item,
|
||||
),
|
||||
})
|
||||
}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Amps"
|
||||
type="number"
|
||||
value={port.amps}
|
||||
size="small"
|
||||
inputProps={{ min: 0, max: 1000 }}
|
||||
onChange={(event) =>
|
||||
onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduOutputPorts: selectedEquipment.pduOutputPorts.map((item) =>
|
||||
item.id === port.id ? { ...item, amps: clampNumber(event.target.value, 0, 1000, port.amps) } : item,
|
||||
),
|
||||
})
|
||||
}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<Tooltip title="Delete port">
|
||||
<IconButton
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduOutputPorts: selectedEquipment.pduOutputPorts.filter((item) => item.id !== port.id),
|
||||
})
|
||||
}
|
||||
>
|
||||
<Delete fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 800 }}>
|
||||
Managed PDU
|
||||
</Typography>
|
||||
<Slider
|
||||
value={selectedEquipment.pduManaged ? 1 : 0}
|
||||
min={0}
|
||||
max={1}
|
||||
step={1}
|
||||
marks={[
|
||||
{ value: 0, label: 'No' },
|
||||
{ value: 1, label: 'Yes' },
|
||||
]}
|
||||
onChange={(_, value) =>
|
||||
onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduManaged: (value as number) === 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{selectedEquipment.pduManaged && (
|
||||
<>
|
||||
<TextField
|
||||
label="IP address / hostname"
|
||||
value={selectedEquipment.pduHostname}
|
||||
onChange={(event) =>
|
||||
onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduHostname: event.target.value,
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="URL"
|
||||
value={selectedEquipment.pduUrl}
|
||||
onChange={(event) =>
|
||||
onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduUrl: event.target.value,
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="User"
|
||||
value={selectedEquipment.pduUser}
|
||||
onChange={(event) =>
|
||||
onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, {
|
||||
pduUser: event.target.value,
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<TextField
|
||||
label="Power watts"
|
||||
type="number"
|
||||
|
||||
@@ -11,6 +11,7 @@ export const componentLibrary: LibraryItem[] = [
|
||||
{ type: 'storage-array', category: 'equipment', label: 'Storage Array', defaultU: 4, minU: 1, maxU: 24, color: '#ffb300' },
|
||||
{ type: 'storage-switch', category: 'equipment', label: 'Storage Switch', defaultU: 1, minU: 1, maxU: 24, color: '#42a5f5' },
|
||||
{ type: 'patch-panel', category: 'equipment', label: 'Patch Panel', defaultU: 1, minU: 1, maxU: 24, color: '#8d6e63' },
|
||||
{ type: 'pdu', category: 'equipment', label: 'PDU', defaultU: 1, minU: 1, maxU: 12, color: '#f97316' },
|
||||
{ type: 'cable-management', category: 'equipment', label: 'Cable Management', defaultU: 1, minU: 1, maxU: 24, color: '#78909c' },
|
||||
{ type: 'kvm', category: 'equipment', label: 'KVM', defaultU: 1, minU: 1, maxU: 24, color: '#66bb6a' },
|
||||
{ type: 'kvm-console', category: 'equipment', label: 'KVM (Console)', defaultU: 1, minU: 1, maxU: 24, color: '#ec407a' },
|
||||
|
||||
@@ -11,6 +11,7 @@ export type ComponentType =
|
||||
| 'storage-array'
|
||||
| 'storage-switch'
|
||||
| 'patch-panel'
|
||||
| 'pdu'
|
||||
| 'cable-management'
|
||||
| 'kvm'
|
||||
| 'kvm-console';
|
||||
@@ -19,6 +20,17 @@ export type LibraryCategory = 'container' | 'equipment';
|
||||
|
||||
export type PatchPanelMedium = '' | 'copper' | 'fiberoptic';
|
||||
|
||||
export type PduMount = 'horizontal' | 'vertical';
|
||||
|
||||
export type PduSide = 'left' | 'right';
|
||||
|
||||
export interface PduOutputPort {
|
||||
id: string;
|
||||
label: string;
|
||||
voltage: number;
|
||||
amps: number;
|
||||
}
|
||||
|
||||
export interface LibraryItem {
|
||||
type: ComponentType;
|
||||
category: LibraryCategory;
|
||||
@@ -43,6 +55,14 @@ export interface Equipment {
|
||||
patchPanelMedium: PatchPanelMedium;
|
||||
patchPanelPorts: number;
|
||||
patchPanelPortIdentification: string;
|
||||
pduMount: PduMount;
|
||||
pduSide: PduSide;
|
||||
pduInputVoltage: string;
|
||||
pduOutputPorts: PduOutputPort[];
|
||||
pduManaged: boolean;
|
||||
pduHostname: string;
|
||||
pduUrl: string;
|
||||
pduUser: string;
|
||||
notes: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
+44
-3
@@ -1,4 +1,4 @@
|
||||
import type { DiagramData, Equipment, RackContainer } from '../types';
|
||||
import type { DiagramData, Equipment, PduOutputPort, RackContainer } from '../types';
|
||||
import { getLibraryItem } from '../data/componentLibrary';
|
||||
|
||||
export const MAX_CONTAINERS = 10;
|
||||
@@ -56,6 +56,14 @@ export const createEquipment = (type: string, rack: RackContainer, preferredU: n
|
||||
patchPanelMedium: libraryItem.type === 'patch-panel' ? 'copper' : '',
|
||||
patchPanelPorts: libraryItem.type === 'patch-panel' ? 24 : 0,
|
||||
patchPanelPortIdentification: '',
|
||||
pduMount: 'horizontal',
|
||||
pduSide: 'right',
|
||||
pduInputVoltage: '',
|
||||
pduOutputPorts: [],
|
||||
pduManaged: false,
|
||||
pduHostname: '',
|
||||
pduUrl: '',
|
||||
pduUser: '',
|
||||
notes: '',
|
||||
color: libraryItem.color,
|
||||
};
|
||||
@@ -93,6 +101,10 @@ export const isRangeAvailable = (
|
||||
}
|
||||
|
||||
return rack.equipment.every((item) => {
|
||||
if (item.type === 'pdu' && item.pduMount === 'vertical') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.id === ignoreEquipmentId) {
|
||||
return true;
|
||||
}
|
||||
@@ -104,7 +116,10 @@ export const isRangeAvailable = (
|
||||
};
|
||||
|
||||
export const getOccupancy = (rack: RackContainer) => {
|
||||
const used = rack.equipment.reduce((sum, item) => sum + item.sizeU, 0);
|
||||
const used = rack.equipment.reduce(
|
||||
(sum, item) => sum + (item.type === 'pdu' && item.pduMount === 'vertical' ? 0 : item.sizeU),
|
||||
0,
|
||||
);
|
||||
return {
|
||||
used,
|
||||
total: rack.sizeU,
|
||||
@@ -141,7 +156,9 @@ export const normalizeDiagram = (value: unknown): DiagramData => {
|
||||
equipment: Array.isArray(rack.equipment)
|
||||
? rack.equipment.map((item, itemIndex) => {
|
||||
const libraryItem = getLibraryItem(item.type) || getLibraryItem('server-rack')!;
|
||||
const sizeU = clampNumber(item.sizeU, 1, 24, libraryItem.defaultU);
|
||||
const pduMount = item.pduMount === 'vertical' ? 'vertical' : 'horizontal';
|
||||
const maxSizeU = libraryItem.type === 'pdu' ? (pduMount === 'vertical' ? 3 : 12) : 24;
|
||||
const sizeU = clampNumber(item.sizeU, 1, maxSizeU, libraryItem.defaultU);
|
||||
return {
|
||||
id: item.id || createId('equipment'),
|
||||
type: libraryItem.type,
|
||||
@@ -161,6 +178,14 @@ export const normalizeDiagram = (value: unknown): DiagramData => {
|
||||
: '',
|
||||
patchPanelPorts: clampNumber(item.patchPanelPorts, 0, 10000, libraryItem.type === 'patch-panel' ? 24 : 0),
|
||||
patchPanelPortIdentification: item.patchPanelPortIdentification || '',
|
||||
pduMount,
|
||||
pduSide: item.pduSide === 'left' ? 'left' : 'right',
|
||||
pduInputVoltage: item.pduInputVoltage || '',
|
||||
pduOutputPorts: normalizePduOutputPorts(item.pduOutputPorts),
|
||||
pduManaged: Boolean(item.pduManaged),
|
||||
pduHostname: item.pduHostname || '',
|
||||
pduUrl: item.pduUrl || '',
|
||||
pduUser: item.pduUser || '',
|
||||
notes: item.notes || '',
|
||||
color: item.color || libraryItem.color,
|
||||
};
|
||||
@@ -170,6 +195,22 @@ export const normalizeDiagram = (value: unknown): DiagramData => {
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePduOutputPorts = (ports: unknown): PduOutputPort[] => {
|
||||
if (!Array.isArray(ports)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ports.map((port, index) => {
|
||||
const candidate = port as Partial<PduOutputPort>;
|
||||
return {
|
||||
id: candidate.id || createId('pdu-port'),
|
||||
label: candidate.label || `Port ${index + 1}`,
|
||||
voltage: clampNumber(candidate.voltage, 0, 1000, 120),
|
||||
amps: clampNumber(candidate.amps, 0, 1000, 10),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const clampNumber = (value: unknown, min: number, max: number, fallback: number) => {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
|
||||
Reference in new issue
Block a user