From f028ed8d4e0b496906c2874b6a1bf1511ccdb5e9 Mon Sep 17 00:00:00 2001 From: Jason Clark Date: Wed, 20 Dec 2017 11:40:34 -0800 Subject: [PATCH 1/3] Support layouts in layouts Create a Panel class to handle all layout Move view creation into its own file Dynamically convert all configs into panels --- lib/dashboard.js | 54 ++----- lib/generate-layouts.js | 113 ++----------- lib/views/index.js | 36 ++++- lib/views/panel.js | 139 ++++++++++++++++ test/lib/generate-layouts.spec.js | 250 +--------------------------- test/lib/views/panel.spec.js | 261 ++++++++++++++++++++++++++++++ 6 files changed, 467 insertions(+), 386 deletions(-) create mode 100644 lib/views/panel.js create mode 100644 test/lib/views/panel.spec.js diff --git a/lib/dashboard.js b/lib/dashboard.js index cdd2a81..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,8 +13,7 @@ var THROTTLE_TIMEOUT = 150; var Dashboard = function Dashboard(options) { this.options = options || {}; - this.views = {}; - this.settings = options.settings; + this.settings = this.options.settings; this.screen = blessed.screen({ smartCSR: true, @@ -32,21 +30,19 @@ var Dashboard = function Dashboard(options) { Dashboard.prototype._createViews = function () { this.layouts = generateLayouts(this.options.layoutsFile); - this.views = []; // 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); }; @@ -126,31 +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) { - if (this.settings[layoutConfig.view.type]) { - layoutConfig = _.merge(layoutConfig, { - view: this.settings[layoutConfig.view.type] - }); - } - var view = new View({ - parent: this.container, - logProvider: this.logProvider, - metricsProvider: this.metricsProvider, - layoutConfig: layoutConfig - }); - - this.views.push(view); - } - }.bind(this)); + // Remove current layout + if (this.panel) { + this.panel.destroy(); + delete this.panel; + } + + // 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/generate-layouts.js b/lib/generate-layouts.js index 091b82c..222e87d 100644 --- a/lib/generate-layouts.js +++ b/lib/generate-layouts.js @@ -1,112 +1,11 @@ "use strict"; + var _ = require("lodash"); var assert = require("assert"); var path = require("path"); var defaultLayoutConfig = require("./default-layout-config"); var validate = require("jsonschema").validate; var layoutConfigSchema = require("./layout-config-schema.json"); -/* eslint-disable no-magic-numbers */ - -// 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); - }, []); -}; module.exports = function generateLayouts(layoutsFile) { var layoutConfig = defaultLayoutConfig; @@ -125,5 +24,13 @@ module.exports = function generateLayouts(layoutsFile) { ); } - return layoutConfig.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/views/index.js b/lib/views/index.js index 050542b..265018c 100644 --- a/lib/views/index.js +++ b/lib/views/index.js @@ -1,21 +1,34 @@ "use strict"; +var _ = require("lodash"); var StreamView = require("./stream-view"); var EventLoopView = require("./eventloop-view"); var MemoryGaugeView = require("./memory-gauge-view"); var MemoryGraphView = require("./memory-graph-view"); var CpuView = require("./cpu-view"); var BaseView = require("./base-view"); +var Panel = require("./panel"); var VIEW_MAP = { log: StreamView, 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) { + options = options || {}; if (VIEW_MAP[options.type]) { return VIEW_MAP[options.type]; } else if (options.module) { @@ -25,6 +38,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 + }); + }); + }); +}); From af3ec221344bb0f964b7afe75164a73026d95969 Mon Sep 17 00:00:00 2001 From: Jason Clark Date: Wed, 20 Dec 2017 12:48:10 -0800 Subject: [PATCH 2/3] Include panels in panels --- lib/layout-config-schema.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/layout-config-schema.json b/lib/layout-config-schema.json index ee34fef..563797f 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" From a08165bccbb0126cbad3a3dd26ff4c8ec8764b1e Mon Sep 17 00:00:00 2001 From: Jason Clark Date: Thu, 28 Dec 2017 11:36:38 -0800 Subject: [PATCH 3/3] create layout --- lib/default-layout-config.js | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) 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" + } + ] + } ] ];