diff --git a/lib/dashboard.js b/lib/dashboard.js index 65267f7..26736f3 100644 --- a/lib/dashboard.js +++ b/lib/dashboard.js @@ -2,7 +2,6 @@ var _ = require("lodash"); var blessed = require("blessed"); - var HelpView = require("./views/help"); var generateLayouts = require("./generate-layouts"); var LogProvider = require("./providers/log-provider"); @@ -14,7 +13,7 @@ var THROTTLE_TIMEOUT = 150; var Dashboard = function Dashboard(options) { this.options = options || {}; - this.views = {}; + this.settings = this.options.settings; this.screen = blessed.screen({ smartCSR: true, @@ -30,22 +29,20 @@ var Dashboard = function Dashboard(options) { }; Dashboard.prototype._createViews = function () { - this.layouts = generateLayouts(this.options.layoutsFile, this.options.settings); - this.views = []; + this.layouts = generateLayouts(this.options.layoutsFile); // container prevents stream view scrolling from interfering with side views this.container = blessed.box(); this.screen.append(this.container); - - this.helpView = new HelpView({ - parent: this.container - }); - - this.gotoTimeView = new GotoTimeView({ - metricsProvider: this.metricsProvider, + this.viewOptions = { + screen: this.screen, parent: this.container, - screen: this.screen - }); + logProvider: this.logProvider, + metricsProvider: this.metricsProvider + }; + + this.helpView = new HelpView(this.viewOptions); + this.gotoTimeView = new GotoTimeView(this.viewOptions); this._showLayout(0); }; @@ -125,26 +122,15 @@ Dashboard.prototype._showLayout = function (id) { if (this.currentLayout === id) { return; } - _.each(this.views, function (view) { - view.destroy(); - }); - - this.views = []; - - _.each(this.layouts[id], function (layoutConfig) { - var View = views.getConstructor(layoutConfig.view); - if (View) { - var view = new View({ - parent: this.container, - logProvider: this.logProvider, - metricsProvider: this.metricsProvider, - layoutConfig: layoutConfig - }); + // Remove current layout + if (this.panel) { + this.panel.destroy(); + delete this.panel; + } - this.views.push(view); - } - }.bind(this)); + // create new layout + this.panel = views.create(this.layouts[id], this.viewOptions, this.settings); this.currentLayout = id; this.helpView.node.setFront(); diff --git a/lib/default-layout-config.js b/lib/default-layout-config.js index 735261b..8458c59 100644 --- a/lib/default-layout-config.js +++ b/lib/default-layout-config.js @@ -120,5 +120,47 @@ module.exports = [ } ] } + ], + [ + { + views: [ + { + position: { + grow: 2 + }, + type: "panel", + views: [ + { + type: "panel", + views: [ + { + type: "nodeDetails" + }, + { + type: "systemDetails" + } + ] + }, + { + type: "panel", + views: [ + { + type: "cpuDetails" + }, + { + type: "userDetails" + } + ] + } + ] + }, + { + position: { + grow: 5 + }, + type: "envDetails" + } + ] + } ] ]; diff --git a/lib/generate-layouts.js b/lib/generate-layouts.js index e8a1642..222e87d 100644 --- a/lib/generate-layouts.js +++ b/lib/generate-layouts.js @@ -7,108 +7,7 @@ var defaultLayoutConfig = require("./default-layout-config"); var validate = require("jsonschema").validate; var layoutConfigSchema = require("./layout-config-schema.json"); -// Each layout consists of vertical panels, that contains its position and horizontal views. -// Flex-like positions of panels and views defined by 'grow' and 'size' parameters. -// View or panel with 'size' has exactly height or width respectively. -// View or panel with 'grow' fills part of the residuary space (it works like flex-grow). -// By default, position = { grow: 1 } - -var normalizePosition = function (position) { - if (!_.has(position, "grow") && !_.has(position, "size")) { - position = { grow: 1 }; - } - - return position; -}; - -var concatPosition = function (position1, position2) { - position1 = normalizePosition(position1); - position2 = normalizePosition(position2); - - return { - grow: (position1.grow || 0) + (position2.grow || 0), - size: (position1.size || 0) + (position2.size || 0) - }; -}; - -var getSummaryPosition = function (items) { - return items.map(function (item) { return item.position; }) - .reduce(concatPosition, { grow: 0, size: 0 }); -}; - -var getSize = function (parentSize, itemPosition) { - var position = normalizePosition(itemPosition.position); - if (_.has(position, "size")) { - return position.size; - } - - // Prevent last growing view from overflowing screen - var round = itemPosition.offset.grow + position.grow === itemPosition.summary.grow ? - Math.floor : Math.ceil; - - return round( - (parentSize - itemPosition.summary.size) * position.grow / itemPosition.summary.grow - ); -}; - -var getOffset = function (parentSize, itemPosition) { - return itemPosition.summary.grow ? Math.ceil( - itemPosition.offset.size + - (parentSize - itemPosition.summary.size) * itemPosition.offset.grow / itemPosition.summary.grow - ) : 0; -}; - -var createViewLayout = function (view, viewPosition, panelPosition) { - return { - view: view, - getPosition: function (parent) { - return { - width: getSize(parent.width, panelPosition), - height: getSize(parent.height, viewPosition), - left: getOffset(parent.width, panelPosition), - top: getOffset(parent.height, viewPosition) - }; - } - }; -}; - -var createPanelLayout = function (panelPosition, views) { - var viewSummaryPosition = getSummaryPosition(views); - var offsetPosition = { size: 0, grow: 0 }; - - return views.map(function (view) { - var viewPosition = { - summary: viewSummaryPosition, - offset: offsetPosition, - position: view.position - }; - - offsetPosition = concatPosition(view.position, offsetPosition); - - return createViewLayout(view, viewPosition, panelPosition); - }); -}; - -var createLayout = function (panelsConfig) { - var panelSummaryPosition = getSummaryPosition(panelsConfig); - var offsetPosition = { size: 0, grow: 0 }; - - return panelsConfig.reduce(function (layouts, panelConfig) { - var panelPosition = { - summary: panelSummaryPosition, - offset: offsetPosition, - position: panelConfig.position - }; - - var viewLayouts = createPanelLayout(panelPosition, panelConfig.views); - - offsetPosition = concatPosition(panelConfig.position, offsetPosition); - - return layouts.concat(viewLayouts); - }, []); -}; - -var loadConfigs = function (layoutsFile) { +module.exports = function generateLayouts(layoutsFile) { var layoutConfig = defaultLayoutConfig; if (layoutsFile) { /* eslint-disable global-require */ @@ -124,23 +23,14 @@ var loadConfigs = function (layoutsFile) { "Layout config is invalid:\n\n * " + validationResult.errors.join("\n * ") + "\n" ); } - return layoutConfig; -}; -var applyCustomizations = function (customizations) { - return function (panelsConfig) { - return panelsConfig.map(function (view) { - var customization = customizations[view.type]; - if (!customization) { - return view; - } - return _.merge(view, { view: customization }); - }); - }; -}; - -module.exports = function generateLayouts(layoutsFile, customizations) { - return loadConfigs(layoutsFile) - .map(applyCustomizations(customizations || {})) - .map(createLayout); + return layoutConfig.map(function (layouts) { + return { + view: { + type: "panel", + views: layouts.map(function (config) { return _.merge(config, { type: "panel" }); }) + }, + getPosition: _.identity + }; + }); }; diff --git a/lib/layout-config-schema.json b/lib/layout-config-schema.json index a93d8fd..2a2a766 100644 --- a/lib/layout-config-schema.json +++ b/lib/layout-config-schema.json @@ -20,6 +20,8 @@ "type": "array", "items": { "oneOf": [{ + "$ref": "#/definitions/panel" + }, { "$ref": "#/definitions/streamView" }, { "$ref": "#/definitions/memoryView" diff --git a/lib/views/index.js b/lib/views/index.js index 4159dea..965cba4 100644 --- a/lib/views/index.js +++ b/lib/views/index.js @@ -1,5 +1,6 @@ "use strict"; +var _ = require("lodash"); var StreamView = require("./stream-view"); var EventLoopView = require("./eventloop-view"); var MemoryGaugeView = require("./memory-gauge-view"); @@ -11,6 +12,7 @@ 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 Panel = require("./panel"); var VIEW_MAP = { cpuDetails: CpuDetailsView, @@ -22,7 +24,17 @@ var VIEW_MAP = { cpu: CpuView, memory: MemoryGaugeView, memoryGraph: MemoryGraphView, - eventLoop: EventLoopView + eventLoop: EventLoopView, + panel: Panel +}; + +// Customize view types based on a settings class +var applyCustomizations = function (customizations, layoutConfig) { + var customization = customizations[layoutConfig.view.type]; + if (!customization) { + return layoutConfig; + } + return _.merge(layoutConfig, { view: customization }); }; var getConstructor = function (options) { @@ -36,6 +48,23 @@ var getConstructor = function (options) { return null; }; -module.exports = { - getConstructor: getConstructor +/** + * Creates a view + * + * @param {Object} layoutConfig raw layout { type, views, position } + * @param {Object} options startup options for views + * @param {Object} customizations view type customiztaions + * + * @returns {Object} created view oject + */ +module.exports.create = function create(layoutConfig, options, customizations) { + var customized = applyCustomizations(customizations, layoutConfig); + var viewOptions = Object.assign({}, options, { + layoutConfig: customized, + creator: function (layout) { + return create(layout, options, customizations); + } + }); + var View = getConstructor(customized.view); + return View ? new View(viewOptions) : null; }; diff --git a/lib/views/panel.js b/lib/views/panel.js new file mode 100644 index 0000000..a4515a6 --- /dev/null +++ b/lib/views/panel.js @@ -0,0 +1,139 @@ +"use strict"; + +var _ = require("lodash"); + +// Each layout consists of vertical panels, that contains its position and horizontal views. +// Flex-like positions of panels and views defined by 'grow' and 'size' parameters. +// View or panel with 'size' has exactly height or width respectively. +// View or panel with 'grow' fills part of the residuary space (it works like flex-grow). +// By default, position = { grow: 1 } + +var normalizePosition = function (position) { + if (!_.has(position, "grow") && !_.has(position, "size")) { + position = { grow: 1 }; + } + + return position; +}; + +var concatPosition = function (position1, position2) { + position1 = normalizePosition(position1); + position2 = normalizePosition(position2); + + return { + grow: (position1.grow || 0) + (position2.grow || 0), + size: (position1.size || 0) + (position2.size || 0) + }; +}; + +var getSummaryPosition = function (items) { + return items.map(function (item) { return item.position; }) + .reduce(concatPosition, { grow: 0, size: 0 }); +}; + +var getSize = function (parentSize, itemPosition) { + var position = normalizePosition(itemPosition.position); + if (_.has(position, "size")) { + return position.size; + } + + // Prevent last growing view from overflowing screen + var round = itemPosition.offset.grow + position.grow === itemPosition.summary.grow ? + Math.floor : Math.ceil; + + return round( + (parentSize - itemPosition.summary.size) * position.grow / itemPosition.summary.grow + ); +}; + +var getOffset = function (parentSize, itemPosition) { + return itemPosition.summary.grow ? Math.ceil( + itemPosition.offset.size + + (parentSize - itemPosition.summary.size) * itemPosition.offset.grow / itemPosition.summary.grow + ) : 0; +}; + +var createViewLayout = function (view, viewPosition, panelPosition) { + return { + view: view, + getPosition: function (parent) { + return { + width: getSize(parent.width, panelPosition), + height: getSize(parent.height, viewPosition), + left: getOffset(parent.width, panelPosition), + top: getOffset(parent.height, viewPosition) + }; + } + }; +}; + +var createPanelLayout = function (panelPosition, views) { + var viewSummaryPosition = getSummaryPosition(views); + var offsetPosition = { size: 0, grow: 0 }; + + return _.flatMap(views, function (view) { + var viewPosition = { + summary: viewSummaryPosition, + offset: offsetPosition, + position: view.position + }; + + offsetPosition = concatPosition(view.position, offsetPosition); + + return createViewLayout(view, viewPosition, panelPosition); + }); +}; + +var createLayout = function (panelsConfig) { + var panelSummaryPosition = getSummaryPosition(panelsConfig); + var offsetPosition = { size: 0, grow: 0 }; + + return panelsConfig.reduce(function (layouts, panelConfig) { + var panelPosition = { + summary: panelSummaryPosition, + offset: offsetPosition, + position: panelConfig.position + }; + + var viewLayouts = createPanelLayout(panelPosition, panelConfig.views); + + offsetPosition = concatPosition(panelConfig.position, offsetPosition); + + return layouts.concat(viewLayouts); + }, []); +}; + +// Child views need their position adjusted to fit inside the panel +var wrapGetPosition = function (viewPosition, panelPosition) { + return function (parent) { + return viewPosition(panelPosition(parent)); + }; +}; + +/** + * A psudeo view that creates sub views and lays them out in columns and rows + * + * @param {Object} options view creation options + * + * @returns {null} The class needs to be created with new + */ +var Panel = function Panel(options) { + var panelLayout = options.layoutConfig; + var viewLayouts = createLayout(panelLayout.view.views); + this.getPosition = panelLayout.getPosition; + this.views = _.map(viewLayouts, function (viewLayout) { + viewLayout.getPosition = wrapGetPosition(viewLayout.getPosition, panelLayout.getPosition); + return options.creator(viewLayout); + }); +}; + +Panel.prototype.destroy = function () { + _.each(this.views, function (view) { + if (view && typeof view.destroy === "function") { + view.destroy(); + } + }); + this.views = []; +}; + +module.exports = Panel; diff --git a/test/lib/generate-layouts.spec.js b/test/lib/generate-layouts.spec.js index 6672625..14ef2d2 100644 --- a/test/lib/generate-layouts.spec.js +++ b/test/lib/generate-layouts.spec.js @@ -14,146 +14,7 @@ var mock = function (path, obj) { var generateLayouts = require("../../lib/generate-layouts"); -var parent = { - width: 17, - height: 13 -}; - describe("generate-layouts", function () { - beforeEach(function () { - mock("fake/empty-layout", []); - mock("fake/invalid-config-layout", { invalid: "config" }); - mock("fake/fill-view-layout", [[ - { - views: [ - { - type: "memory" - } - ] - } - ]]); - mock("fake/exact-width-panel-layout", [[ - { - position: { - size: 11 - }, - views: [ - { - type: "memory" - } - ] - } - ]]); - mock("fake/grow-panels-layout", [[ - { - position: { - grow: 2 - }, - views: [ - { - type: "memory" - } - ] - }, - { - position: { - grow: 3 - }, - views: [ - { - type: "memory" - } - ] - } - ]]); - mock("fake/mixed-panels-layout", [[ - { - position: { - grow: 2 - }, - views: [ - { - type: "memory" - } - ] - }, - { - position: { - size: 4 - }, - views: [ - { - type: "memory" - } - ] - }, - { - position: { - grow: 3 - }, - views: [ - { - type: "memory" - } - ] - } - ]]); - mock("fake/exact-height-view-layout", [[ - { - views: [ - { - position: { - size: 11 - }, - type: "memory" - } - ] - } - ]]); - mock("fake/grow-views-layout", [[ - { - views: [ - { - position: { - grow: 2 - }, - type: "memory" - }, - { - position: { - grow: 3 - }, - type: "memory" - } - ] - } - ]]); - mock("fake/mixed-views-layout", [[ - { - views: [ - { - position: { - grow: 2 - }, - type: "memory" - }, - { - position: { - size: 4 - }, - type: "memory" - }, - { - position: { - grow: 3 - }, - type: "memory" - } - ] - } - ]]); - }); - it("should validate default layout", function () { expect(generateLayouts("lib/default-layout-config.js")).to.be.an("array"); }); @@ -164,118 +25,21 @@ describe("generate-layouts", function () { }).to.throw(/Cannot find module/); expect(function () { + mock("fake/invalid-config-layout", { invalid: "config" }); generateLayouts("fake/invalid-config-layout"); }).to.throw(/instance is not of a type\(s\) array/); }); it("should generate empty layout", function () { + mock("fake/empty-layout", []); expect(generateLayouts("fake/empty-layout")).to.be.empty; }); - it("should create fullscreen view", function () { - var layouts = generateLayouts("fake/fill-view-layout"); - expect(layouts[0][0]).to.have.property("getPosition").that.is.a("function"); - expect(layouts[0][0].getPosition(parent)).to.be.deep.equal({ - left: 0, - top: 0, - width: parent.width, - height: parent.height - }); - }); - - it("should create exact width panel", function () { - var layouts = generateLayouts("fake/exact-width-panel-layout"); - expect(layouts[0][0].getPosition(parent)).to.be.deep.equal({ - left: 0, - top: 0, - width: 11, - height: parent.height - }); - }); - - it("should create growing panels", function () { - var layouts = generateLayouts("fake/grow-panels-layout"); - expect(layouts[0][0].getPosition(parent)).to.be.deep.equal({ - left: 0, - top: 0, - width: 7, - height: parent.height - }); - expect(layouts[0][1].getPosition(parent)).to.be.deep.equal({ - left: 7, - top: 0, - width: 10, - height: parent.height - }); - }); - - it("should create mixed width panels", function () { - var layouts = generateLayouts("fake/mixed-panels-layout"); - expect(layouts[0][0].getPosition(parent)).to.be.deep.equal({ - left: 0, - top: 0, - width: 6, - height: parent.height - }); - expect(layouts[0][1].getPosition(parent)).to.be.deep.equal({ - left: 6, - top: 0, - width: 4, - height: parent.height - }); - expect(layouts[0][2].getPosition(parent)).to.be.deep.equal({ - left: 10, - top: 0, - width: 7, - height: parent.height - }); - }); - - it("should create exact height view", function () { - var layouts = generateLayouts("fake/exact-height-view-layout"); - expect(layouts[0][0].getPosition(parent)).to.be.deep.equal({ - left: 0, - top: 0, - width: parent.width, - height: 11 - }); - }); - - it("should create growing views", function () { - var layouts = generateLayouts("fake/grow-views-layout"); - expect(layouts[0][0].getPosition(parent)).to.be.deep.equal({ - left: 0, - top: 0, - width: parent.width, - height: 6 - }); - expect(layouts[0][1].getPosition(parent)).to.be.deep.equal({ - left: 0, - top: 6, - width: parent.width, - height: 7 - }); - }); + it("should include a getPosition method", function () { + var layout = generateLayouts("lib/default-layout-config.js"); + var fake = { fake: "result" }; - it("should create mixed height views", function () { - var layouts = generateLayouts("fake/mixed-views-layout"); - expect(layouts[0][0].getPosition(parent)).to.be.deep.equal({ - left: 0, - top: 0, - width: parent.width, - height: 4 - }); - expect(layouts[0][1].getPosition(parent)).to.be.deep.equal({ - left: 0, - top: 4, - width: parent.width, - height: 4 - }); - expect(layouts[0][2].getPosition(parent)).to.be.deep.equal({ - left: 0, - top: 8, - width: parent.width, - height: 5 - }); + expect(layout[0]).to.respondTo("getPosition"); + expect(layout[0].getPosition(fake)).to.eql(fake); }); }); diff --git a/test/lib/views/panel.spec.js b/test/lib/views/panel.spec.js new file mode 100644 index 0000000..98fcf39 --- /dev/null +++ b/test/lib/views/panel.spec.js @@ -0,0 +1,261 @@ +"use strict"; + +var _ = require("lodash"); +var expect = require("chai").expect; + +var Panel = require("../../../lib/views/panel"); + +/* eslint-disable no-magic-numbers */ +describe("Panel", function () { + + var parent = { + top: 0, + left: 0, + width: 17, + height: 13 + }; + + var createPanel = function (layouts) { + var views = layouts.map(function (config) { + return _.merge({ type: "panel" }, config); + }); + + return new Panel({ + layoutConfig: { + view: { + type: "panel", + views: views + }, + getPosition: _.identity + }, + creator: _.identity + }); + }; + + describe("layout panel", function () { + it("should create fullscreen view", function () { + var layouts = createPanel([{ + views: [ + { + type: "memory" + } + ] + }]); + expect(layouts.views[0]).to.have.property("getPosition").that.is.a("function"); + expect(layouts.views[0].getPosition(parent)).to.be.deep.equal({ + left: 0, + top: 0, + width: parent.width, + height: parent.height + }); + }); + + it("should create exact width panel", function () { + var layouts = createPanel([{ + position: { + size: 11 + }, + views: [ + { + type: "memory" + } + ] + }]); + expect(layouts.views[0].getPosition(parent)).to.be.deep.equal({ + left: 0, + top: 0, + width: 11, + height: parent.height + }); + }); + + it("should create growing panels", function () { + var layouts = createPanel([ + { + position: { + grow: 2 + }, + views: [ + { + type: "memory" + } + ] + }, + { + position: { + grow: 3 + }, + views: [ + { + type: "memory" + } + ] + } + ]); + expect(layouts.views[0].getPosition(parent)).to.be.deep.equal({ + left: 0, + top: 0, + width: 7, + height: parent.height + }); + expect(layouts.views[1].getPosition(parent)).to.be.deep.equal({ + left: 7, + top: 0, + width: 10, + height: parent.height + }); + }); + + it("should create mixed width panels", function () { + var layouts = createPanel([ + { + position: { + grow: 2 + }, + views: [ + { + type: "memory" + } + ] + }, + { + position: { + size: 4 + }, + views: [ + { + type: "memory" + } + ] + }, + { + position: { + grow: 3 + }, + views: [ + { + type: "memory" + } + ] + } + ]); + expect(layouts.views[0].getPosition(parent)).to.be.deep.equal({ + left: 0, + top: 0, + width: 6, + height: parent.height + }); + expect(layouts.views[1].getPosition(parent)).to.be.deep.equal({ + left: 6, + top: 0, + width: 4, + height: parent.height + }); + expect(layouts.views[2].getPosition(parent)).to.be.deep.equal({ + left: 10, + top: 0, + width: 7, + height: parent.height + }); + }); + + it("should create exact height view", function () { + var layouts = createPanel([ + { + views: [ + { + position: { + size: 11 + }, + type: "memory" + } + ] + } + ]); + expect(layouts.views[0].getPosition(parent)).to.be.deep.equal({ + left: 0, + top: 0, + width: parent.width, + height: 11 + }); + }); + + it("should create growing views", function () { + var layouts = createPanel([ + { + views: [ + { + position: { + grow: 2 + }, + type: "memory" + }, + { + position: { + grow: 3 + }, + type: "memory" + } + ] + } + ]); + expect(layouts.views[0].getPosition(parent)).to.be.deep.equal({ + left: 0, + top: 0, + width: parent.width, + height: 6 + }); + expect(layouts.views[1].getPosition(parent)).to.be.deep.equal({ + left: 0, + top: 6, + width: parent.width, + height: 7 + }); + }); + + it("should create mixed height views", function () { + var layouts = createPanel([ + { + views: [ + { + position: { + grow: 2 + }, + type: "memory" + }, + { + position: { + size: 4 + }, + type: "memory" + }, + { + position: { + grow: 3 + }, + type: "memory" + } + ] + } + ]); + expect(layouts.views[0].getPosition(parent)).to.be.deep.equal({ + left: 0, + top: 0, + width: parent.width, + height: 4 + }); + expect(layouts.views[1].getPosition(parent)).to.be.deep.equal({ + left: 0, + top: 4, + width: parent.width, + height: 4 + }); + expect(layouts.views[2].getPosition(parent)).to.be.deep.equal({ + left: 0, + top: 8, + width: parent.width, + height: 5 + }); + }); + }); +});