Compare commits

...

2 Commits

@ -4,7 +4,7 @@
"description": "A VPS management panel",
"main": "index.js",
"scripts": {
"dev": "NODE_ENV=development nodemon --ext js,pug --ignore node_modules --ignore src/client bin/server.js"
"dev": "NODE_ENV=development nodemon --ext js,pug,jsx --ignore node_modules --ignore src/client bin/server.js"
},
"repository": {
"type": "git",
@ -13,6 +13,8 @@
"author": "Sven Slootweg",
"license": "WTFPL",
"dependencies": {
"@babel/register": "^7.4.0",
"@joepie91/express-react-views": "^1.0.1",
"@joepie91/gulp-partial-patch-livereload-logger": "^1.0.1",
"JSONStream": "^1.1.4",
"array.prototype.flat": "^1.2.1",
@ -23,13 +25,16 @@
"body-parser": "^1.15.2",
"capitalize": "^2.0.0",
"checkit": "^0.7.0",
"classnames": "^2.2.6",
"create-error": "^0.3.1",
"create-event-emitter": "^1.0.0",
"dataloader": "^1.4.0",
"debounce": "^1.0.0",
"debug": "^4.1.1",
"default-value": "^1.0.0",
"dotty": "^0.1.0",
"end-of-stream": "^1.1.0",
"escape-string-regexp": "^2.0.0",
"execall": "^1.0.0",
"express": "^4.14.0",
"express-promise-router": "^1.1.0",
@ -40,8 +45,10 @@
"joi": "^14.3.0",
"knex": "^0.13.0",
"map-obj": "^3.0.0",
"memoizee": "^0.4.14",
"pg": "^6.1.0",
"pug": "^2.0.0-beta6",
"react-dom": "^16.8.6",
"rfr": "^1.2.3",
"scrypt-for-humans": "^2.0.5",
"snake-case": "^2.1.0",
@ -76,7 +83,7 @@
"json-loader": "^0.5.4",
"listening": "^0.1.0",
"nodemon": "^1.18.11",
"react": "^16.6.3",
"react": "^16.8.6",
"react-hot-loader": "^4.3.12",
"riot": "^3.6.1",
"riotjs-loader": "^4.0.0",

@ -1,11 +1,14 @@
'use strict';
const Promise = require("bluebird");
const express = require("express");
// const expressWs = require("express-ws");
const knex = require("knex");
const path = require("path");
const bodyParser = require("body-parser");
const expressAsyncReact = require("./express-async-react");
function projectPath(targetPath) {
return path.join(__dirname, "..", targetPath);
}
@ -14,14 +17,45 @@ module.exports = function () {
let db = knex(require("../knexfile"));
let imageStore = require("./image-store")(projectPath("./images"));
let taskTracker = require("../lib/tasks/tracker")();
let apiQuery = require("./api")();
let state = {db, imageStore, taskTracker};
let app = express();
// expressWs(app);
app.set("view engine", "pug");
app.set("views", projectPath("views"));
app.engine("jsx", expressAsyncReact.createEngine({
prepare: (template, locals) => {
return Promise.try(() => {
if (template.query != null) {
let queryArguments = (template.queryArguments != null)
? template.queryArguments(locals)
: {};
return apiQuery(template.query, queryArguments);
}
}).then((result) => {
if (result == null) {
return {};
} else {
if (result.errors != null && result.errors.length > 0) {
throw result.errors[0];
} else {
return {
data: result.data
};
}
}
});
}
}));
app.set("view engine", "jsx");
app.set("views", projectPath("src/views"));
app.locals[expressAsyncReact.Settings] = {
componentPath: "template"
};
app.use((req, res, next) => {
res.locals.isUnderPrefix = function isUnderPrefix(path, resultingClass) {
@ -31,7 +65,7 @@ module.exports = function () {
} else {
return "";
}
}
};
next();
});

@ -0,0 +1,30 @@
"use strict";
const escapeStringRegexp = require("escape-string-regexp");
const assureArray = require("assure-array");
let viewPathRegexes = new Map();
function generateRegex(paths) {
let pathRegexes = assureArray(paths)
.map((path) => `^${escapeStringRegexp(path)}`)
.join("|");
return new RegExp(pathRegexes);
}
function clearCacheByRegex(regex) {
for (let [key, entry] in Object.entries(regex)) {
if (regex.test(entry.filename) === true) {
delete require.cache[key];
}
}
}
module.exports = function clearRequireCache(viewPaths) {
if (!viewPathRegexes.has(viewPaths)) {
viewPathRegexes.set(viewPathRegexes, generateRegex(viewPaths));
}
clearCacheByRegex(viewPathRegexes.get(viewPathRegexes));
};

@ -0,0 +1,79 @@
"use strict";
const Promise = require("bluebird");
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const defaultValue = require("default-value");
const dotty = require("dotty");
const registerBabel = require("./register-babel");
const clearRequireCache = require("./clear-require-cache");
/* FILEBUG: Express does not copy symbol-keyed properties from app.locals, but it probably should? */
// let ExpressReact = Symbol("ExpressReact");
let ExpressReact = "__EXPRESS_REACT_SETTINGS";
let LocalsContext = React.createContext({});
function componentAtPath(moduleRoot, componentPath) {
if (componentPath == null) {
return moduleRoot;
} else {
return dotty.get(moduleRoot, componentPath);
}
}
function renderComponent(component, locals, doctype = "<!DOCTYPE html>") {
let tree = React.createElement(LocalsContext.Provider, {value: locals},
React.createElement(component, locals)
);
try {
/* TODO: Expose internals? Like koa-react */
return doctype + ReactDOMServer.renderToStaticMarkup(tree);
} catch (err) {
if (/Element type is invalid:/.test(err.message)) {
throw new Error(`Expected a React component, but got '${component}' - maybe you forgot to specify a componentPath?`);
} else {
throw err;
}
}
}
module.exports = {
Settings: ExpressReact,
LocalsContext: LocalsContext,
createEngine: function createEngine({ prepare } = {}) {
let babelRegistered = false;
return function asyncRender(filename, options, callback) {
return Promise.try(() => {
let viewPaths = options.settings.views;
if (!babelRegistered) {
registerBabel(viewPaths);
babelRegistered = true;
}
let {componentPath} = defaultValue(options[ExpressReact], {});
return Promise.try(() => {
let templateFile = require(filename);
let moduleRoot = defaultValue(templateFile.exports, templateFile);
return Promise.try(() => {
if (prepare != null) {
return prepare(moduleRoot, options);
}
}).then((result) => {
let mergedOptions = Object.assign({}, options, result);
return renderComponent(componentAtPath(moduleRoot, componentPath), mergedOptions);
});
}).finally(() => {
if (options.settings.env === "development") {
clearRequireCache(viewPaths);
}
});
}).asCallback(callback);
}
}
};

@ -0,0 +1,20 @@
"use strict";
const assureArray = require("assure-array");
const babelRegister = require("@babel/register");
module.exports = function registerBabel(viewPaths, options = {}) {
let babelOptions = Object.assign({
only: assureArray(viewPaths),
presets: [
"@babel/preset-react",
["@babel/preset-env", {
targets: {
node: "current"
}
}]
]
}, options);
babelRegister(babelOptions);
};

@ -53,26 +53,27 @@ module.exports = function({db}) {
let router = require("express-promise-router")();
router.get("/", (req, res) => {
return Promise.try(() => {
return getStorageDevices();
}).then((devices) => {
/* FIXME: Auto-formatting of total sizes and units */
let fixedDrives = devices.filter((drive) => drive.removable === false);
let removableDrives = devices.filter((drive) => drive.removable === true);
// return Promise.try(() => {
// return getStorageDevices();
// }).then((devices) => {
// /* FIXME: Auto-formatting of total sizes and units */
// let fixedDrives = devices.filter((drive) => drive.removable === false);
// let removableDrives = devices.filter((drive) => drive.removable === true);
let healthyFixedDrives = fixedDrives.filter((drive) => drive.smartStatus === "healthy");
let deterioratingFixedDrives = fixedDrives.filter((drive) => drive.smartStatus === "deteriorating");
let failingFixedDrives = fixedDrives.filter((drive) => drive.smartStatus === "failing");
// let healthyFixedDrives = fixedDrives.filter((drive) => drive.smartStatus === "healthy");
// let deterioratingFixedDrives = fixedDrives.filter((drive) => drive.smartStatus === "deteriorating");
// let failingFixedDrives = fixedDrives.filter((drive) => drive.smartStatus === "failing");
res.render("hardware/storage-devices/list", {
devices: devices,
totalFixedStorage: roundUnit(B(sumDriveSizes(fixedDrives)).toTiB()),
totalHealthyFixedStorage: roundUnit(B(sumDriveSizes(healthyFixedDrives)).toTiB()),
totalDeterioratingFixedStorage: roundUnit(B(sumDriveSizes(deterioratingFixedDrives)).toTiB()),
totalFailingFixedStorage: roundUnit(B(sumDriveSizes(failingFixedDrives)).toTiB()),
totalRemovableStorage: roundUnit(B(sumDriveSizes(removableDrives)).toGiB())
});
});
// res.render("hardware/storage-devices/list", {
// devices: devices,
// totalFixedStorage: roundUnit(B(sumDriveSizes(fixedDrives)).toTiB()),
// totalHealthyFixedStorage: roundUnit(B(sumDriveSizes(healthyFixedDrives)).toTiB()),
// totalDeterioratingFixedStorage: roundUnit(B(sumDriveSizes(deterioratingFixedDrives)).toTiB()),
// totalFailingFixedStorage: roundUnit(B(sumDriveSizes(failingFixedDrives)).toTiB()),
// totalRemovableStorage: roundUnit(B(sumDriveSizes(removableDrives)).toGiB())
// });
// });
res.render("hardware/storage-devices/list");
});
return router;

@ -0,0 +1,15 @@
"use strict";
const React = require("react");
module.exports = {
template: function ErrorPage({ error }) {
return (
<div className="error">
<h1>An error occurred.</h1>
<h2>{ error.message }</h2>
<pre>{ error.stack }</pre>
</div>
);
}
};

@ -0,0 +1,109 @@
"use strict";
const React = require("react");
const Layout = require("../../layout");
const gql = require("../../../graphql/tag");
module.exports = {
query: gql`
query {
hardware {
drives {
path
model
modelFamily
}
}
}
`,
template: function StorageDeviceList({data}) {
return (
<Layout>
<div className="fancyStuff">
<ul>
{data.hardware.drives.map((drive) => {
return <li>
{drive.path}: {drive.model} ({drive.modelFamily})
</li>;
})}
</ul>
</div>
</Layout>
);
}
};
// extends ../../layout
// block content
// h2 Fixed drives
// //- FIXME: Partitions with mountpoints
// table.drives
// tr
// th SMART
// th Device
// th Total size
// th RPM
// th Serial number
// th Model
// th Family
// th Firmware version
// for device in devices.filter((device) => device.removable === false)
// tr(class=(device.children.length > 0 ? "hasPartitions" : null))
// td(class=`smart ${device.smartStatus}`, rowspan=(1 + device.children.length))
// td= device.name
// td= device.size
// td #{device.information.rpm} RPM
// td= device.information.serialNumber
// td= device.information.model
// td= device.information.modelFamily
// td= device.information.firmwareVersion
// for partition, i in device.children
// tr.partition(class=(i === device.children.length - 1) ? "last" : null)
// td= partition.name
// td= partition.size
// td(colspan=5)
// if partition.mountpoint != null
// = partition.mountpoint
// else
// span.notMounted (not mounted)
// //- tr.partition
// //- td(colspan=8)= JSON.stringify(partition)
// tr
// th(colspan=2) Total
// td= totalFixedStorage
// td(colspan=5).hidden
// tr.smartStatus
// th(colspan=2).healthy Healthy
// td= totalHealthyFixedStorage
// td(colspan=5).hidden
// tr.smartStatus
// th(colspan=2).atRisk At-risk
// td= totalDeterioratingFixedStorage
// td(colspan=5).hidden
// tr.smartStatus
// th(colspan=2).failing Failing
// td= totalFailingFixedStorage
// td(colspan=5).hidden
// h2 Removable drives
// table
// tr
// th Path
// th Total size
// th Mounted at
// for device in devices.filter((device) => device.type === "loopDevice")
// tr
// td= device.path
// td= device.size
// td= device.mountpoint

@ -0,0 +1,45 @@
"use strict";
const React = require("react");
const classnames = require("classnames");
// const {LocalsContext} = require("../express-async-react");
function MenuItem({ path, children }) {
let isActive = false; // FIXME
return (
<div className={classnames("menu-item", {active: isActive})}>
<a href={path}>
{children}
</a>
</div>
);
}
module.exports = function Layout({ children }) {
// let locals = React.useContext(LocalsContext);
return (
<html>
<head>
<title>CVM</title>
<link rel="stylesheet" href="/css/style.css"/>
</head>
<body>
<div className="menu">
<h1>CVM</h1>
<MenuItem path="/disk-images">Disk Images</MenuItem>
<MenuItem path="/instances">Instances</MenuItem>
<MenuItem path="/users">Users</MenuItem>
</div>
<div className="content">
{children}
</div>
<script src="/js/bundle.js" />
<script src="/budo/livereload.js" />
</body>
</html>
);
};

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save