diff --git a/lib/constants.js b/lib/constants.js index 2ea66c1..5a638ca 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -23,6 +23,8 @@ var AGGREGATE_TIME_LEVELS = [ "3600000" ]; +var MILLISECONDS_PER_SECOND = 1000; + // this array object is used to reduce ms to its highest human-readable form // see lib/providers/metrics-provider.js::getTimeIndexLabel var TIME_SCALES = [ @@ -49,5 +51,6 @@ var TIME_SCALES = [ module.exports = { AGGREGATE_TIME_LEVELS: AGGREGATE_TIME_LEVELS, + MILLISECONDS_PER_SECOND: MILLISECONDS_PER_SECOND, TIME_SCALES: TIME_SCALES }; diff --git a/lib/layout-config-schema.json b/lib/layout-config-schema.json index ee34fef..a93d8fd 100644 --- a/lib/layout-config-schema.json +++ b/lib/layout-config-schema.json @@ -27,6 +27,16 @@ "$ref": "#/definitions/lineGraphView" }, { "$ref": "#/definitions/customView" + }, { + "$ref": "#/definitions/cpuDetailsView" + }, { + "$ref": "#/definitions/envDetailsView" + }, { + "$ref": "#/definitions/nodeDetailsView" + }, { + "$ref": "#/definitions/systemDetailsView" + }, { + "$ref": "#/definitions/userDetailsView" }] } } @@ -125,6 +135,96 @@ }, "required": ["module"] }, + "cpuDetailsView": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "borderColor": { + "type": "string" + }, + "type": { + "enum": ["cpuDetails"] + }, + "position": { + "$ref": "#/definitions/position" + } + }, + "required": ["type"] + }, + "envDetailsView": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "borderColor": { + "type": "string" + }, + "type": { + "enum": ["envDetails"] + }, + "position": { + "$ref": "#/definitions/position" + } + }, + "required": ["type"] + }, + "nodeDetailsView": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "borderColor": { + "type": "string" + }, + "type": { + "enum": ["nodeDetails"] + }, + "position": { + "$ref": "#/definitions/position" + } + }, + "required": ["type"] + }, + "systemDetailsView": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "borderColor": { + "type": "string" + }, + "type": { + "enum": ["systemDetails"] + }, + "position": { + "$ref": "#/definitions/position" + } + }, + "required": ["type"] + }, + "userDetailsView": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "borderColor": { + "type": "string" + }, + "type": { + "enum": ["userDetails"] + }, + "position": { + "$ref": "#/definitions/position" + } + }, + "required": ["type"] + }, "position": { "oneOf": [{ "type": "object", diff --git a/lib/views/base-details-view.js b/lib/views/base-details-view.js new file mode 100644 index 0000000..2713921 --- /dev/null +++ b/lib/views/base-details-view.js @@ -0,0 +1,54 @@ +"use strict"; + +var blessed = require("blessed"); +var _ = require("lodash"); +var BaseView = require("./base-view"); + +var BaseDetailsView = function BaseDetailsView(options) { + BaseView.call(this, options); + + this.screen = options.parent.screen; + this.node = blessed.box(this.layoutConfig); + this.parent.append(this.node); + + this.refreshContent(); + this.recalculatePosition(); +}; + +BaseDetailsView.prototype = Object.create(BaseView.prototype); + +BaseDetailsView.prototype.refreshContent = function () { + this.node.setContent(this._getBoxContent(this.getDetails())); + this.screen.render(); +}; + +BaseDetailsView.prototype.getDetails = function () { + return []; +}; + +/** + * Given data and optional filters, return the content for a box. + * + * @param {Object[]} data + * This is the array of label/data objects that define each data + * point for the box. + * + * @returns {String} + * The content string for the box is returned. + */ +BaseDetailsView.prototype._getBoxContent = function (data) { + var longestLabel = _.reduce(data, function (prev, detail) { + return Math.max(prev, detail.label.length); + }, 0); + + var getFormattedContent = function (prev, details) { + prev += "{cyan-fg}{bold}" + details.label + "{/}" + + _.repeat(" ", longestLabel - details.label.length + 1) + + "{green-fg}" + details.data + "{/}\n"; + return prev; + }; + + return _.trimEnd(_.reduce(data, getFormattedContent, ""), "\n"); +}; + +module.exports = BaseDetailsView; diff --git a/lib/views/cpu-details-view.js b/lib/views/cpu-details-view.js new file mode 100644 index 0000000..4f6528c --- /dev/null +++ b/lib/views/cpu-details-view.js @@ -0,0 +1,37 @@ +"use strict"; + +var os = require("os"); +var _ = require("lodash"); +var BaseDetailsView = require("./base-details-view"); + +var CpuDetailsView = function CpuDetailsView(options) { + BaseDetailsView.call(this, options); +}; + +CpuDetailsView.prototype = Object.create(BaseDetailsView.prototype); + +CpuDetailsView.prototype.getDetails = function () { + var cpuInfo = os.cpus(); + + return _.map(cpuInfo, function (info, index) { + return { + label: "[" + index + "]", + data: info.model + " " + info.speed + }; + }); +}; + +CpuDetailsView.prototype.getDefaultLayoutConfig = function () { + return { + label: " CPU(s) ", + border: "line", + tags: true, + style: { + border: { + fg: "white" + } + } + }; +}; + +module.exports = CpuDetailsView; diff --git a/lib/views/env-details-view.js b/lib/views/env-details-view.js new file mode 100644 index 0000000..17fa26f --- /dev/null +++ b/lib/views/env-details-view.js @@ -0,0 +1,47 @@ +"use strict"; + +var _ = require("lodash"); +var BaseDetailsView = require("./base-details-view"); + +var EnvDetailsView = function EnvDetailsView(options) { + BaseDetailsView.call(this, options); +}; + +EnvDetailsView.prototype = Object.create(BaseDetailsView.prototype); + +EnvDetailsView.prototype.getDefaultLayoutConfig = function () { + return { + label: " Environment Variables ", + border: "line", + style: { + border: { + fg: "white" + } + }, + tags: true, + scrollable: true, + keys: true, + input: true, + scrollbar: { + style: { + fg: "white", + inverse: true + }, + track: { + ch: ":", + fg: "cyan" + } + } + }; +}; + +EnvDetailsView.prototype.getDetails = function () { + return _.map(process.env, function (value, key) { + return { + label: key, + data: value + }; + }); +}; + +module.exports = EnvDetailsView; diff --git a/lib/views/index.js b/lib/views/index.js index 050542b..4159dea 100644 --- a/lib/views/index.js +++ b/lib/views/index.js @@ -6,8 +6,18 @@ var MemoryGaugeView = require("./memory-gauge-view"); var MemoryGraphView = require("./memory-graph-view"); var CpuView = require("./cpu-view"); var BaseView = require("./base-view"); +var CpuDetailsView = require("./cpu-details-view"); +var EnvDetailsView = require("./env-details-view"); +var NodeDetailsView = require("./node-details-view"); +var SystemDetailsView = require("./system-details-view"); +var UserDetailsView = require("./user-details-view"); var VIEW_MAP = { + cpuDetails: CpuDetailsView, + envDetails: EnvDetailsView, + nodeDetails: NodeDetailsView, + systemDetails: SystemDetailsView, + userDetails: UserDetailsView, log: StreamView, cpu: CpuView, memory: MemoryGaugeView, @@ -16,6 +26,7 @@ var VIEW_MAP = { }; var getConstructor = function (options) { + options = options || {}; if (VIEW_MAP[options.type]) { return VIEW_MAP[options.type]; } else if (options.module) { diff --git a/lib/views/node-details-view.js b/lib/views/node-details-view.js new file mode 100644 index 0000000..dafa332 --- /dev/null +++ b/lib/views/node-details-view.js @@ -0,0 +1,59 @@ +"use strict"; + +var BaseDetailsView = require("./base-details-view"); +var time = require("../time"); +var MILLISECONDS_PER_SECOND = require("../constants").MILLISECONDS_PER_SECOND; +var UPTIME_INTERVAL_MS = MILLISECONDS_PER_SECOND; + +var NodeDetailsView = function NodeDetailsView(options) { + BaseDetailsView.call(this, options); + + this.setupdate(); + this.node.on("attach", this.setupdate.bind(this)); + + this.node.on("detach", function () { + if (this.uptimeInterval) { + clearInterval(this.uptimeInterval); + delete this.uptimeInterval; + } + }.bind(this)); +}; + +NodeDetailsView.prototype = Object.create(BaseDetailsView.prototype); + +NodeDetailsView.prototype.setupdate = function () { + this.uptimeInterval = this.uptimeInterval || setInterval(function () { + this.refreshContent(); + }.bind(this), UPTIME_INTERVAL_MS); +}; + +NodeDetailsView.prototype.getDetails = function () { + return [ + { + label: "Version", + data: process.version + }, { + label: "LTS", + data: process.release.lts + }, { + label: "Uptime", + data: time.getLabel(process.uptime() * MILLISECONDS_PER_SECOND) + } + ]; +}; + +NodeDetailsView.prototype.getDefaultLayoutConfig = function () { + return { + label: " Node ", + border: "line", + tags: true, + height: "shrink", + style: { + border: { + fg: "white" + } + } + }; +}; + +module.exports = NodeDetailsView; diff --git a/lib/views/system-details-view.js b/lib/views/system-details-view.js new file mode 100644 index 0000000..55682ee --- /dev/null +++ b/lib/views/system-details-view.js @@ -0,0 +1,54 @@ +"use strict"; + +var os = require("os"); +var prettyBytes = require("pretty-bytes"); +var BaseDetailsView = require("./base-details-view"); + +var SystemDetailsView = function SystemDetailsView(options) { + BaseDetailsView.call(this, options); +}; + +SystemDetailsView.prototype = Object.create(BaseDetailsView.prototype); + +SystemDetailsView.prototype.getDetails = function () { + return [ + { + label: "Architecture", + data: os.arch() + }, { + label: "Endianness", + data: os.endianness() === "BE" ? "Big Endian" : "Little Endian" + }, { + label: "Host Name", + data: os.hostname() + }, { + label: "Total Memory", + data: prettyBytes(os.totalmem()) + }, { + label: "Platform", + data: os.platform() + }, { + label: "Release", + data: os.release() + }, { + label: "Type", + data: os.type() + } + ]; +}; + +SystemDetailsView.prototype.getDefaultLayoutConfig = function () { + return { + label: " System ", + border: "line", + tags: true, + height: "shrink", + style: { + border: { + fg: "white" + } + } + }; +}; + +module.exports = SystemDetailsView; diff --git a/lib/views/user-details-view.js b/lib/views/user-details-view.js new file mode 100644 index 0000000..216de27 --- /dev/null +++ b/lib/views/user-details-view.js @@ -0,0 +1,59 @@ +"use strict"; + +var os = require("os"); +var BaseDetailsView = require("./base-details-view"); + +var UserDetailsView = function UserDetailsView(options) { + BaseDetailsView.call(this, options); +}; + +UserDetailsView.prototype = Object.create(BaseDetailsView.prototype); + +UserDetailsView.prototype.getDefaultLayoutConfig = function () { + return { + label: " User ", + border: "line", + tags: true, + height: "shrink", + style: { + border: { + fg: "white" + } + } + }; +}; + +UserDetailsView.prototype.getDetails = function () { + // Node version 6 added userInfo function + if (!os.userInfo) { + return [ + { + label: "User Information", + data: "Not supported on this version of Node" + } + ]; + } + + var userInfo = os.userInfo({ encoding: "utf8" }); + + return [ + { + label: "User Name", + data: userInfo.username + }, { + label: "Home", + data: userInfo.homedir + }, { + label: "User ID", + data: userInfo.uid + }, { + label: "Group ID", + data: userInfo.gid + }, { + label: "Shell", + data: userInfo.shell + } + ]; +}; + +module.exports = UserDetailsView; diff --git a/test/lib/views/cpu-details-view.spec.js b/test/lib/views/cpu-details-view.spec.js new file mode 100644 index 0000000..81d9c5e --- /dev/null +++ b/test/lib/views/cpu-details-view.spec.js @@ -0,0 +1,44 @@ +"use strict"; + +var expect = require("chai").expect; +var sinon = require("sinon"); + +var CpuDetailsView = require("../../../lib/views/cpu-details-view"); +var utils = require("../../utils"); + +describe("CpuDetailsView", function () { + + var sandbox; + var testContainer; + var view; + + before(function () { + sandbox = sinon.sandbox.create(); + }); + + beforeEach(function () { + utils.stubWidgets(sandbox); + testContainer = utils.getTestContainer(sandbox); + view = new CpuDetailsView({ + layoutConfig: { + getPosition: sandbox.stub() + }, + parent: testContainer + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe("getDetails", function () { + it("should include labels", function () { + var details = view.getDetails(); + expect(details).to.be.an("array"); + var labels = details.map(function (detail) { + return detail.label; + }).sort(); + expect(labels).to.include("[0]"); + }); + }); +}); diff --git a/test/lib/views/env-details-view.spec.js b/test/lib/views/env-details-view.spec.js new file mode 100644 index 0000000..c3ac4f1 --- /dev/null +++ b/test/lib/views/env-details-view.spec.js @@ -0,0 +1,40 @@ +"use strict"; + +var expect = require("chai").expect; +var sinon = require("sinon"); + +var EnvDetailsView = require("../../../lib/views/env-details-view"); +var utils = require("../../utils"); + +describe("EnvDetailsView", function () { + + var sandbox; + var testContainer; + var view; + + before(function () { + sandbox = sinon.sandbox.create(); + }); + + beforeEach(function () { + utils.stubWidgets(sandbox); + testContainer = utils.getTestContainer(sandbox); + view = new EnvDetailsView({ + layoutConfig: { + getPosition: sandbox.stub() + }, + parent: testContainer + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe("getDetails", function () { + it("should include labels", function () { + var details = view.getDetails(); + expect(details).to.be.an("array"); + }); + }); +}); diff --git a/test/lib/views/node-details-view.spec.js b/test/lib/views/node-details-view.spec.js new file mode 100644 index 0000000..ca630cc --- /dev/null +++ b/test/lib/views/node-details-view.spec.js @@ -0,0 +1,48 @@ +"use strict"; + +var expect = require("chai").expect; +var sinon = require("sinon"); + +var NodeDetailsView = require("../../../lib/views/node-details-view"); +var utils = require("../../utils"); + +describe("NodeDetailsView", function () { + + var sandbox; + var testContainer; + var view; + + before(function () { + sandbox = sinon.sandbox.create(); + }); + + beforeEach(function () { + utils.stubWidgets(sandbox); + testContainer = utils.getTestContainer(sandbox); + view = new NodeDetailsView({ + layoutConfig: { + getPosition: sandbox.stub() + }, + parent: testContainer + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe("getDetails", function () { + it("should include labels", function () { + var details = view.getDetails(); + expect(details).to.be.an("array"); + var labels = details.map(function (detail) { + return detail.label; + }).sort(); + expect(labels).to.eql([ + "LTS", + "Uptime", + "Version" + ]); + }); + }); +}); diff --git a/test/lib/views/system-details-view.spec.js b/test/lib/views/system-details-view.spec.js new file mode 100644 index 0000000..997f463 --- /dev/null +++ b/test/lib/views/system-details-view.spec.js @@ -0,0 +1,52 @@ +"use strict"; + +var expect = require("chai").expect; +var sinon = require("sinon"); + +var SystemDetailsView = require("../../../lib/views/system-details-view"); +var utils = require("../../utils"); + +describe("SystemDetailsView", function () { + + var sandbox; + var testContainer; + var view; + + before(function () { + sandbox = sinon.sandbox.create(); + }); + + beforeEach(function () { + utils.stubWidgets(sandbox); + testContainer = utils.getTestContainer(sandbox); + view = new SystemDetailsView({ + layoutConfig: { + getPosition: sandbox.stub() + }, + parent: testContainer + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe("getDetails", function () { + it("should include labels", function () { + var details = view.getDetails(); + expect(details).to.be.an("array"); + var labels = details.map(function (detail) { + return detail.label; + }).sort(); + expect(labels).to.eql([ + "Architecture", + "Endianness", + "Host Name", + "Platform", + "Release", + "Total Memory", + "Type" + ]); + }); + }); +}); diff --git a/test/lib/views/user-details-view.spec.js b/test/lib/views/user-details-view.spec.js new file mode 100644 index 0000000..eb9ea37 --- /dev/null +++ b/test/lib/views/user-details-view.spec.js @@ -0,0 +1,40 @@ +"use strict"; + +var expect = require("chai").expect; +var sinon = require("sinon"); + +var UserDetailsView = require("../../../lib/views/user-details-view"); +var utils = require("../../utils"); + +describe("UserDetailsView", function () { + + var sandbox; + var testContainer; + var view; + + before(function () { + sandbox = sinon.sandbox.create(); + }); + + beforeEach(function () { + utils.stubWidgets(sandbox); + testContainer = utils.getTestContainer(sandbox); + view = new UserDetailsView({ + layoutConfig: { + getPosition: sandbox.stub() + }, + parent: testContainer + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe("getDetails", function () { + it("should include labels", function () { + var details = view.getDetails(); + expect(details).to.be.an("array"); + }); + }); +});