|
|
|
"use strict";
|
|
|
|
|
|
|
|
const React = require("react");
|
|
|
|
const classnames = require("classnames");
|
|
|
|
const syncpipe = require("syncpipe");
|
|
|
|
const splitFilterN = require("split-filter-n");
|
|
|
|
const { B } = require("../../../packages/unit-bytes-iec");
|
|
|
|
const treecutter = require("../../../packages/treecutter");
|
|
|
|
|
|
|
|
const Layout = require("../layout.jsx");
|
|
|
|
|
|
|
|
function sum(values) {
|
|
|
|
return values.reduce((total, value) => total + value, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
function sumDriveSizes(drives) {
|
|
|
|
return syncpipe(drives, [
|
|
|
|
(_) => _.map((drive) => drive.size.toB().amount),
|
|
|
|
(_) => sum(_),
|
|
|
|
(_) => B(_)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
function Indented({ depth, children }) {
|
|
|
|
return (
|
|
|
|
<div style={{ paddingLeft: depth * 10 }}>
|
|
|
|
{children}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function MountEntry({ mount }) {
|
|
|
|
return <div className="mountpoint">{mount.mountpoint}</div>;
|
|
|
|
}
|
|
|
|
|
|
|
|
function PartitionEntry({partition, isLast}) {
|
|
|
|
function PartitionIndent({ children }) {
|
|
|
|
return (
|
|
|
|
<Indented depth={partition._treecutterDepth}>
|
|
|
|
{children}
|
|
|
|
</Indented>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<tr className={classnames("partition", {last: isLast})}>
|
|
|
|
<td>
|
|
|
|
<PartitionIndent>
|
|
|
|
{partition.name}
|
|
|
|
</PartitionIndent>
|
|
|
|
</td>
|
|
|
|
<td>
|
|
|
|
<PartitionIndent>
|
|
|
|
{partition.size.toDisplay(2).toString()}
|
|
|
|
</PartitionIndent>
|
|
|
|
</td>
|
|
|
|
<td colSpan={5}>
|
|
|
|
<PartitionIndent>
|
|
|
|
{(partition.mounts.length > 0)
|
|
|
|
? partition.mounts.map((mount) => <MountEntry mount={mount} />)
|
|
|
|
: <span className="notMounted">(not mounted)</span>
|
|
|
|
}
|
|
|
|
</PartitionIndent>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function DriveEntry({drive}) {
|
|
|
|
let flattenedPartitions = treecutter.flatten(drive.partitions);
|
|
|
|
let hasPartitions = (flattenedPartitions.length > 0);
|
|
|
|
|
|
|
|
return (<>
|
|
|
|
<tr className={classnames({hasPartitions})}>
|
|
|
|
<td className={classnames("smart", drive.smartHealth)} rowSpan={1 + flattenedPartitions.length} />
|
|
|
|
<td>{drive.path}</td>
|
|
|
|
<td>{drive.size.toDisplay(2).toString()}</td>
|
|
|
|
<td>
|
|
|
|
{(drive.rpm != null)
|
|
|
|
? `${drive.rpm} RPM`
|
|
|
|
: null
|
|
|
|
}
|
|
|
|
</td>
|
|
|
|
<td>{drive.serialNumber}</td>
|
|
|
|
<td>{drive.model}</td>
|
|
|
|
<td>{drive.modelFamily}</td>
|
|
|
|
<td>{drive.firmwareVersion}</td>
|
|
|
|
</tr>
|
|
|
|
{flattenedPartitions.map((partition, i) => {
|
|
|
|
let isLast = (i === flattenedPartitions.length - 1);
|
|
|
|
|
|
|
|
return <PartitionEntry partition={partition} isLast={isLast} />;
|
|
|
|
})}
|
|
|
|
</>);
|
|
|
|
}
|
|
|
|
|
|
|
|
function TallyRow({ label, rowClass, labelClass, children }) {
|
|
|
|
return (
|
|
|
|
<tr className={rowClass}>
|
|
|
|
<th colspan="2" className={labelClass}>{label}</th>
|
|
|
|
<td>{children}</td>
|
|
|
|
<td className="hidden" colspan="5"></td>
|
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
query: {
|
|
|
|
hardware: {
|
|
|
|
drives: {
|
|
|
|
path: true,
|
|
|
|
smartHealth: true,
|
|
|
|
size: true,
|
|
|
|
rpm: true,
|
|
|
|
serialNumber: true,
|
|
|
|
model: true,
|
|
|
|
modelFamily: true,
|
|
|
|
firmwareVersion: true,
|
|
|
|
|
|
|
|
blockDevice: {
|
|
|
|
name: true
|
|
|
|
},
|
|
|
|
|
|
|
|
partitions: {
|
|
|
|
$key: "allBlockDevices",
|
|
|
|
name: true,
|
|
|
|
size: true,
|
|
|
|
|
|
|
|
mounts: {
|
|
|
|
mountpoint: true
|
|
|
|
},
|
|
|
|
|
|
|
|
children: {
|
|
|
|
$recurse: true,
|
|
|
|
$recurseLimit: Infinity, // 3 by default
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
template: function StorageDeviceList({data}) {
|
|
|
|
let drivesByStatus = splitFilterN(data.hardware.drives, [ "HEALTHY", "DETERIORATING", "FAILING", "UNKNOWN" ], (drive) => drive.smartHealth);
|
|
|
|
|
|
|
|
let totalStorage = sumDriveSizes(data.hardware.drives);
|
|
|
|
let totalHealthyStorage = sumDriveSizes(drivesByStatus.HEALTHY);
|
|
|
|
let totalAtRiskStorage = sumDriveSizes(drivesByStatus.DETERIORATING);
|
|
|
|
let totalFailingStorage = sumDriveSizes(drivesByStatus.FAILING);
|
|
|
|
let totalUnknownStorage = sumDriveSizes(drivesByStatus.UNKNOWN);
|
|
|
|
|
|
|
|
// TODO: Show unallocated space
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Layout title="Storage Devices">
|
|
|
|
<table className="drives">
|
|
|
|
<tr>
|
|
|
|
<th>SMART</th>
|
|
|
|
<th>Device</th>
|
|
|
|
<th>Total size</th>
|
|
|
|
<th>RPM</th>
|
|
|
|
<th>Serial number</th>
|
|
|
|
<th>Model</th>
|
|
|
|
<th>Family</th>
|
|
|
|
<th>Firmware version</th>
|
|
|
|
</tr>
|
|
|
|
{data.hardware.drives.map((drive) => <DriveEntry drive={drive} />)}
|
|
|
|
<TallyRow label="Total">
|
|
|
|
{totalStorage.toDisplay(2).toString()}
|
|
|
|
</TallyRow>
|
|
|
|
<TallyRow label="Healthy" rowClass="smartStatus" labelClass="healthy">
|
|
|
|
{totalHealthyStorage.toDisplay(2).toString()}
|
|
|
|
</TallyRow>
|
|
|
|
<TallyRow label="At-risk" rowClass="smartStatus" labelClass="atRisk">
|
|
|
|
{totalAtRiskStorage.toDisplay(2).toString()}
|
|
|
|
</TallyRow>
|
|
|
|
<TallyRow label="Failing" rowClass="smartStatus" labelClass="failing">
|
|
|
|
{totalFailingStorage.toDisplay(2).toString()}
|
|
|
|
</TallyRow>
|
|
|
|
<TallyRow label="Unknown" rowClass="smartStatus" labelClass="unknown">
|
|
|
|
{totalUnknownStorage.toDisplay(2).toString()}
|
|
|
|
</TallyRow>
|
|
|
|
</table>
|
|
|
|
</Layout>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|