diff --git a/package.json b/package.json index f37a394..7b2ae1b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A VPS management panel", "main": "index.js", "scripts": { - "dev": "NODE_ENV=development nodemon --ext js,pug,jsx --ignore node_modules --ignore src/client bin/server.js" + "dev": "NODE_ENV=development nodemon --ext js,pug,jsx,gql --ignore node_modules --ignore src/client bin/server.js" }, "repository": { "type": "git", diff --git a/public/css/style.css b/public/css/style.css index 44384a0..bc56563 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -11,22 +11,45 @@ label { .menu { background-color: #000424; } - .menu h1, .menu .menu-item { + .menu h1, .menu .menuItem { display: inline-block; color: white; } .menu h1 { margin: 0px 16px; } - .menu .menu-item a { + .menu .menuItem a { color: white; text-decoration: none; padding: 15px 9px 5px 9px; } - .menu .menu-item.active a { - background-color: #e4e4e4; + .menu .menuItem.active a { + /* FIXME: Make this lighter when there is no submenu, to match the page background color */ + background-color: #dddddd; color: black; } - .menu .menu-item:not(.active) a:hover { + .menu .menuItem:not(.active) a:hover { background-color: #afafaf; color: black; } +.fakeSubmenu, .submenu { + background: linear-gradient(to bottom, #dddddd, #dddddd 60%, #caccce); } + +.fakeSubmenu { + height: 16px; } + +.submenu { + padding: .3em .2em 0 .2em; + border-bottom: 1px solid #000424; } + .submenu .menuItem { + display: inline-block; + margin-bottom: -1px; + padding: .3em .7em; + font-size: .95em; } + .submenu .menuItem a { + text-decoration: none; + color: black; } + .submenu .menuItem.active { + background-color: #e4e4e4; + border: 1px solid #000424; + border-bottom: none; } + table { border-collapse: collapse; } table th, table td { @@ -37,13 +60,13 @@ table { table td.hidden { border: none; } -table.drives td.smart.healthy { +table.drives td.smart.HEALTHY { background-color: #00a500; } -table.drives td.smart.deteriorating { +table.drives td.smart.DETERIORATING { background-color: #ff9100; } -table.drives td.smart.failing { +table.drives td.smart.FAILING { background-color: #e60000; } table.drives .hasPartitions td:not(.smart), table.drives .partition:not(.last) td:not(.smart) { diff --git a/src/api/data-sources/lsblk.js b/src/api/data-sources/lsblk.js index 54a3e10..c15f558 100644 --- a/src/api/data-sources/lsblk.js +++ b/src/api/data-sources/lsblk.js @@ -3,27 +3,10 @@ const Promise = require("bluebird"); const memoizee = require("memoizee"); +const linearizeTree = require("../../linearize-tree"); const lsblk = require("../../wrappers/lsblk"); const All = require("../../graphql/symbols/all"); -function linearizeDevices(devices) { - let linearizedDevices = []; - - function add(list) { - for (let device of list) { - linearizedDevices.push(device); - - if (device.children != null) { - add(device.children); - } - } - } - - add(devices); - - return linearizedDevices; -} - module.exports = function () { let lsblkOnce = memoizee(() => { return Promise.try(() => { @@ -31,7 +14,7 @@ module.exports = function () { }).then((devices) => { return { tree: devices, - list: linearizeDevices(devices) + list: linearizeTree(devices) }; }); }); @@ -40,6 +23,7 @@ module.exports = function () { return Promise.try(() => { return lsblkOnce(); }).then(({tree, list}) => { + return names.map((name) => { if (name === All) { return tree; diff --git a/src/api/types/block-device.js b/src/api/types/block-device.js index 3d7c97d..d4269b3 100644 --- a/src/api/types/block-device.js +++ b/src/api/types/block-device.js @@ -1,15 +1,15 @@ "use strict"; const {createDataObject, LocalProperties, ID} = require("../../graphql/data-object"); -const matchOrError = require("../../match-or-error"); +const deviceNameFromPath = require("../../device-name-from-path"); +const mapValue = require("../../map-value"); module.exports = function (_types) { return function BlockDevice({ name, path }) { if (name != null) { path = `/dev/${name}`; } else if (path != null) { - let match = matchOrError(/^\/dev\/(.+)$/, path); - name = match[0]; + name = deviceNameFromPath(path); } /* FIXME: parent */ @@ -21,6 +21,13 @@ module.exports = function (_types) { lsblk: { [ID]: name, name: "name", + type: (device) => { + return mapValue(device.type, { + partition: "PARTITION", + disk: "DISK", + loopDevice: "LOOP_DEVICE" + }); + }, size: "size", mountpoint: "mountpoint", deviceNumber: "deviceNumber", diff --git a/src/api/types/drive.js b/src/api/types/drive.js index a19dbd5..f7610d2 100644 --- a/src/api/types/drive.js +++ b/src/api/types/drive.js @@ -1,7 +1,11 @@ "use strict"; +const Promise = require("bluebird"); + const {createDataObject, LocalProperties, ID} = require("../../graphql/data-object"); const upperSnakeCase = require("../../upper-snake-case"); +const linearizeTree = require("../../linearize-tree"); +const deviceNameFromPath = require("../../device-name-from-path"); module.exports = function (types) { return function Drive({ path }) { @@ -13,6 +17,25 @@ module.exports = function (types) { }, /* FIXME: allBlockDevices, for representing every single block device that's hosted on this physical drive, linearly. Need to figure out how that works with representation of mdraid arrays, LVM volumes, etc. */ }, + lsblk: { + [ID]: deviceNameFromPath(path), + allBlockDevices: function (rootDevice, { type }, context) { + let devices = linearizeTree([rootDevice]) + .map((device) => types.BlockDevice({ name: device.name })); + + if (type != null) { + return Promise.filter(devices, (device) => { + return Promise.try(() => { + return device.type({}, context); + }).then((deviceType) => { + return (deviceType === type); + }); + }); + } else { + return devices; + } + } + }, smartctlScan: { [ID]: path, interface: "interface" diff --git a/src/device-name-from-path.js b/src/device-name-from-path.js new file mode 100644 index 0000000..2f41ed9 --- /dev/null +++ b/src/device-name-from-path.js @@ -0,0 +1,8 @@ +"use strict"; + +const matchOrError = require("./match-or-error"); + +module.exports = function deviceNameFromPath(path) { + let [name] = matchOrError(/^\/dev\/(.+)$/, path); + return name; +}; diff --git a/src/graphql/data-object.js b/src/graphql/data-object.js index d7483c7..a425f93 100644 --- a/src/graphql/data-object.js +++ b/src/graphql/data-object.js @@ -9,7 +9,9 @@ function withProperty(dataSource, id, property) { } function withData(dataSource, id, callback) { - return function (_, {data}) { + return function (args, context) { + let {data} = context; + return Promise.try(() => { if (data[dataSource] != null) { return data[dataSource].load(id); @@ -18,7 +20,7 @@ function withData(dataSource, id, callback) { } }).then((value) => { if (value != null) { - return callback(value); + return callback(value, args, context); } else { throw new Error(`Got a null value from data source '${dataSource}' for ID '${id}'`); } diff --git a/src/linearize-tree.js b/src/linearize-tree.js new file mode 100644 index 0000000..b68d6c7 --- /dev/null +++ b/src/linearize-tree.js @@ -0,0 +1,19 @@ +"use strict"; + +module.exports = function linearizeTree(rootList, childrenProperty = "children") { + let linearizedItems = []; + + function add(list) { + for (let item of list) { + linearizedItems.push(item); + + if (item[childrenProperty] != null) { + add(item[childrenProperty]); + } + } + } + + add(rootList); + + return linearizedItems; +}; \ No newline at end of file diff --git a/src/make-units.js b/src/make-units.js index 57b5e2d..a528385 100644 --- a/src/make-units.js +++ b/src/make-units.js @@ -27,10 +27,36 @@ module.exports = function makeUnits(unitSpecs) { } }, toString: function () { - /* TODO: Make this auto-convert */ return `${this.amount} ${this.unit}`; }, - } + toDisplay: function (decimals) { + let roundingFactor = Math.pow(10, decimals); + let unitMagnitude = i; + let amount = this.amount; + let unitsLeft = (i < unitSpecs.length - 1); + + function createOfCurrentMagnitude() { + let roundedValue = Math.round(amount * roundingFactor) / roundingFactor; + + return resultObject[unitSpecs[unitMagnitude].unit](roundedValue); + } + + while (unitsLeft === true) { + let currentUnit = unitSpecs[unitMagnitude]; + + if (amount < currentUnit.toNext) { + return createOfCurrentMagnitude(); + } else { + amount = amount / currentUnit.toNext; + unitMagnitude += 1; + + unitsLeft = (unitMagnitude < unitSpecs.length - 1); + } + } + + return createOfCurrentMagnitude; + } + }; unitSpecs.forEach((otherSpec, otherI) => { let factor = 1; @@ -61,7 +87,7 @@ module.exports = function makeUnits(unitSpecs) { amount: value }); } - } + }; }); return resultObject; diff --git a/src/prefix-title.js b/src/prefix-title.js new file mode 100644 index 0000000..04e6634 --- /dev/null +++ b/src/prefix-title.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = function prefixTitle(prefix, title) { + if (title == null) { + return title; + } else { + return `${prefix} ${title}`; + } +}; \ No newline at end of file diff --git a/src/schemas/main.gql b/src/schemas/main.gql index 948ca59..4876b75 100644 --- a/src/schemas/main.gql +++ b/src/schemas/main.gql @@ -282,8 +282,15 @@ type SmartAttribute { updatedWhen: SmartAttributeUpdateType! } +enum BlockDeviceType { + DISK + PARTITION + LOOP_DEVICE +} + type BlockDevice { name: String! + type: BlockDeviceType! path: String! mountpoint: String deviceNumber: String! @@ -298,6 +305,7 @@ type PhysicalDrive { path: String! interface: String! blockDevice: BlockDevice! + allBlockDevices(type: BlockDeviceType): [BlockDevice!]! smartAvailable: Boolean! smartEnabled: Boolean smartHealth: SmartHealth diff --git a/src/scss/style.scss b/src/scss/style.scss index 7944e67..072c779 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -1,5 +1,9 @@ +$bodyBackgroundColor: rgb(228, 228, 228); +$menuBackgroundColor: rgb(0, 4, 36); +$submenuBackgroundColor: rgb(221, 221, 221); + body { - background-color: rgb(228, 228, 228); + background-color: $bodyBackgroundColor; margin: 0px; font-family: sans-serif; } @@ -17,9 +21,9 @@ label { } .menu { - background-color: rgb(0, 4, 36); + background-color: $menuBackgroundColor; - h1, .menu-item { + h1, .menuItem { display: inline-block; color: white; } @@ -28,7 +32,7 @@ label { margin: 0px 16px; } - .menu-item { + .menuItem { a { color: white; text-decoration: none; @@ -37,7 +41,9 @@ label { &.active { a { - background-color: rgb(228, 228, 228); + // background-color: rgb(228, 228, 228); + /* FIXME: Make this lighter when there is no submenu, to match the page background color */ + background-color: $submenuBackgroundColor; color: black; } } @@ -51,6 +57,39 @@ label { } } +.fakeSubmenu, .submenu { + background: linear-gradient(to bottom, $submenuBackgroundColor, $submenuBackgroundColor 60%, rgb(202, 204, 206)); +} + +.fakeSubmenu { + height: 16px; +} + +.submenu { + padding: .3em .2em 0 .2em; + border-bottom: 1px solid $menuBackgroundColor; + + .menuItem { + display: inline-block; + margin-bottom: -1px; + padding: .3em .7em; + font-size: .95em; + + a { + text-decoration: none; + color: black; + } + + // background-color: red; + + &.active { + background-color: $bodyBackgroundColor; + border: 1px solid $menuBackgroundColor; + border-bottom: none; + } + } +} + table { border-collapse: collapse; @@ -70,15 +109,15 @@ table { table.drives { td.smart { - &.healthy { + &.HEALTHY { background-color: rgb(0, 165, 0); } - &.deteriorating { + &.DETERIORATING { background-color: rgb(255, 145, 0); } - &.failing { + &.FAILING { background-color: rgb(230, 0, 0); } } diff --git a/src/views/components/menu-item.jsx b/src/views/components/menu-item.jsx new file mode 100644 index 0000000..d7ce9b8 --- /dev/null +++ b/src/views/components/menu-item.jsx @@ -0,0 +1,20 @@ +"use strict"; + +const React = require("react"); +const classnames = require("classnames"); + +const {LocalsContext} = require("../../express-async-react"); +const isUnderPrefix = require("../../is-under-prefix"); + +module.exports = function MenuItem({ path, children }) { + let {currentPath} = React.useContext(LocalsContext); + let isActive = isUnderPrefix(path, currentPath); + + return ( +
{ error.stack }-
{ error.stack }+
SMART | +Device | +Total size | +RPM | +Serial number | +Model | +Family | +Firmware version | +
---|