')[0];
+ testHistories = testKeys.reduce(function (histories, key, index) {
+ histories[key] = { key: key, range: index + 10, domain: key + index };
+ return histories;
+ }, {});
+
+ mockComposition =
+ jasmine.createSpyObj('composition', ['load', 'on', 'off']);
+ mockMetadata =
+ jasmine.createSpyObj('metadata', ['valuesForHints']);
+
+ mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
+ mockUnsubscribes = testKeys.reduce(function (map, key) {
+ map[key] = jasmine.createSpy('unsubscribe-' + key);
+ return map;
+ }, {});
+
+ mockmct.composition.get.andReturn(mockComposition);
+ mockComposition.load.andCallFake(function () {
+ testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
+ return Promise.resolve(testChildren);
+ });
+
+ mockmct.telemetry.getMetadata.andReturn(mockMetadata);
+ mockmct.telemetry.getValueFormatter.andCallFake(function (metadatum) {
+ var mockFormatter = jasmine.createSpyObj('formatter', ['format']);
+ mockFormatter.format.andCallFake(function (datum) {
+ return datum[metadatum.hint];
+ });
+ return mockFormatter;
+ });
+ mockmct.telemetry.limitEvaluator.andReturn(mockEvaluator);
+ mockmct.telemetry.subscribe.andCallFake(function (obj, callback) {
+ var key = obj.identifier.key;
+ callbacks[key] = callback;
+ return mockUnsubscribes[key];
+ });
+ mockmct.telemetry.request.andCallFake(function (obj, request) {
+ var key = obj.identifier.key;
+ return Promise.resolve([testHistories[key]]);
+ });
+ mockMetadata.valuesForHints.andCallFake(function (hints) {
+ return [{ hint: hints[0] }];
+ });
+
+ view = provider.view(testObject);
+ view.show(testContainer);
+
+ waitsForChange();
+ });
+
+ it("populates its container", function () {
+ expect(testContainer.children.length > 0).toBe(true);
+ });
+
+ describe("when rows have been populated", function () {
+ function rowsMatch() {
+ var rows = $(testContainer).find(".l-autoflow-row").length;
+ return rows === testChildren.length;
+ }
+
+ it("shows one row per child object", function () {
+ waitsFor(rowsMatch);
+ });
+
+ it("adds rows on composition change", function () {
+ var child = {
+ identifier: { namespace: "test", key: "123" },
+ name: "Object 123"
+ };
+ testChildren.push(child);
+ emitEvent(mockComposition, 'add', child);
+ waitsFor(rowsMatch);
+ });
+
+ it("removes rows on composition change", function () {
+ var child = testChildren.pop();
+ emitEvent(mockComposition, 'remove', child.identifier);
+ waitsFor(rowsMatch);
+ });
+ });
+
+ it("removes subscriptions when destroyed", function () {
+ testKeys.forEach(function (key) {
+ expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
+ });
+ view.destroy();
+ testKeys.forEach(function (key) {
+ expect(mockUnsubscribes[key]).toHaveBeenCalled();
+ });
+ });
+
+ it("provides a button to change column width", function () {
+ var initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
+ var nextWidth =
+ initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
+
+ expect($(testContainer).find('.l-autoflow-col').css('width'))
+ .toEqual(initialWidth + 'px');
+
+ $(testContainer).find('.change-column-width').click();
+
+ waitsFor(function () {
+ var width = $(testContainer).find('.l-autoflow-col').css('width');
+ return width !== initialWidth + 'px';
+ });
+
+ runs(function () {
+ expect($(testContainer).find('.l-autoflow-col').css('width'))
+ .toEqual(nextWidth + 'px');
+ });
+ });
+
+ it("subscribes to all child objects", function () {
+ testKeys.forEach(function (key) {
+ expect(callbacks[key]).toEqual(jasmine.any(Function));
+ });
+ });
+
+ it("displays historical telemetry", function () {
+ waitsFor(function () {
+ return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
+ });
+
+ runs(function () {
+ testKeys.forEach(function (key, index) {
+ var datum = testHistories[key];
+ var $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
+ expect($cell.text()).toEqual(String(datum.range));
+ });
+ });
+ });
+
+ it("displays incoming telemetry", function () {
+ var testData = testKeys.map(function (key, index) {
+ return { key: key, range: index * 100, domain: key + index };
+ });
+
+ testData.forEach(function (datum) {
+ callbacks[datum.key](datum);
+ });
+
+ waitsForChange();
+
+ runs(function () {
+ testData.forEach(function (datum, index) {
+ var $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
+ expect($cell.text()).toEqual(String(datum.range));
+ });
+ });
+ });
+
+ it("updates classes for limit violations", function () {
+ var testClass = "some-limit-violation";
+ mockEvaluator.evaluate.andReturn({ cssClass: testClass });
+ testKeys.forEach(function (key) {
+ callbacks[key]({ range: 'foo', domain: 'bar' });
+ });
+
+ waitsForChange();
+
+ runs(function () {
+ testKeys.forEach(function (datum, index) {
+ var $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
+ expect($cell.hasClass(testClass)).toBe(true);
+ });
+ });
+ });
+
+ it("automatically flows to new columns", function () {
+ var rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
+ var sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
+ var count = testKeys.length;
+ var $container = $(testContainer);
+
+ function columnsHaveAutoflowed() {
+ var itemsHeight = $container.find('.l-autoflow-items').height();
+ var availableHeight = itemsHeight - sliderHeight;
+ var availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
+ var columns = Math.ceil(count / availableRows);
+ return $container.find('.l-autoflow-col').length === columns;
+ }
+
+ $container.find('.abs').css({
+ position: 'absolute',
+ left: '0px',
+ right: '0px',
+ top: '0px',
+ bottom: '0px'
+ });
+ $container.css({ position: 'absolute' });
+
+ runs($container.appendTo.bind($container, document.body));
+ for (var height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
+ runs($container.css.bind($container, 'height', height + 'px'));
+ waitsFor(columnsHaveAutoflowed);
+ }
+ runs($container.remove.bind($container));
+ });
+
+ it("loads composition exactly once", function () {
+ var testObj = testChildren.pop();
+ emitEvent(mockComposition, 'remove', testObj.identifier);
+ testChildren.push(testObj);
+ emitEvent(mockComposition, 'add', testObj);
+ expect(mockComposition.load.calls.length).toEqual(1);
+ });
+ });
+ });
+ });
+});
diff --git a/src/plugins/autoflow/AutoflowTabularRowController.js b/src/plugins/autoflow/AutoflowTabularRowController.js
new file mode 100644
index 0000000000..9058860c82
--- /dev/null
+++ b/src/plugins/autoflow/AutoflowTabularRowController.js
@@ -0,0 +1,94 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2017, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define([], function () {
+ /**
+ * Controller for individual rows of an Autoflow Tabular View.
+ * Subscribes to telemetry and updates row data.
+ *
+ * @param {DomainObject} domainObject the object being viewed
+ * @param {*} data the view data
+ * @param openmct a reference to the openmct application
+ * @param {Function} callback a callback to invoke with "last updated" timestamps
+ */
+ function AutoflowTabularRowController(domainObject, data, openmct, callback) {
+ this.domainObject = domainObject;
+ this.data = data;
+ this.openmct = openmct;
+ this.callback = callback;
+
+ this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
+ this.ranges = this.metadata.valuesForHints(['range']);
+ this.domains = this.metadata.valuesForHints(['domain']);
+ this.rangeFormatter =
+ this.openmct.telemetry.getValueFormatter(this.ranges[0]);
+ this.domainFormatter =
+ this.openmct.telemetry.getValueFormatter(this.domains[0]);
+ this.evaluator =
+ this.openmct.telemetry.limitEvaluator(this.domainObject);
+
+ this.initialized = false;
+ }
+
+ /**
+ * Update row to reflect incoming telemetry data.
+ * @private
+ */
+ AutoflowTabularRowController.prototype.updateRowData = function (datum) {
+ var violations = this.evaluator.evaluate(datum, this.ranges[0]);
+
+ this.initialized = true;
+ this.data.classes = violations ? violations.cssClass : "";
+ this.data.value = this.rangeFormatter.format(datum);
+ this.callback(this.domainFormatter.format(datum));
+ };
+
+ /**
+ * Activate this controller; begin listening for changes.
+ */
+ AutoflowTabularRowController.prototype.activate = function () {
+ this.unsubscribe = this.openmct.telemetry.subscribe(
+ this.domainObject,
+ this.updateRowData.bind(this)
+ );
+
+ this.openmct.telemetry.request(
+ this.domainObject,
+ { size: 1 }
+ ).then(function (history) {
+ if (!this.initialized && history.length > 0) {
+ this.updateRowData(history[history.length - 1]);
+ }
+ }.bind(this));
+ };
+
+ /**
+ * Destroy this controller; detach any associated resources.
+ */
+ AutoflowTabularRowController.prototype.destroy = function () {
+ if (this.unsubscribe) {
+ this.unsubscribe();
+ }
+ };
+
+ return AutoflowTabularRowController;
+});
diff --git a/src/plugins/autoflow/AutoflowTabularView.js b/src/plugins/autoflow/AutoflowTabularView.js
new file mode 100644
index 0000000000..b495824a28
--- /dev/null
+++ b/src/plugins/autoflow/AutoflowTabularView.js
@@ -0,0 +1,125 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2017, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define([
+ './AutoflowTabularController',
+ './AutoflowTabularConstants',
+ '../../ui/VueView',
+ 'text!./autoflow-tabular.html'
+], function (
+ AutoflowTabularController,
+ AutoflowTabularConstants,
+ VueView,
+ autoflowTemplate
+) {
+ var ROW_HEIGHT = AutoflowTabularConstants.ROW_HEIGHT;
+ var SLIDER_HEIGHT = AutoflowTabularConstants.SLIDER_HEIGHT;
+ var INITIAL_COLUMN_WIDTH = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
+ var MAX_COLUMN_WIDTH = AutoflowTabularConstants.MAX_COLUMN_WIDTH;
+ var COLUMN_WIDTH_STEP = AutoflowTabularConstants.COLUMN_WIDTH_STEP;
+
+ /**
+ * Implements the Autoflow Tabular view of a domain object.
+ */
+ function AutoflowTabularView(domainObject, openmct) {
+ var data = {
+ items: [],
+ columns: [],
+ width: INITIAL_COLUMN_WIDTH,
+ filter: "",
+ updated: "No updates",
+ rowCount: 1
+ };
+ var controller =
+ new AutoflowTabularController(domainObject, data, openmct);
+ var interval;
+
+ VueView.call(this, {
+ data: data,
+ methods: {
+ increaseColumnWidth: function () {
+ data.width += COLUMN_WIDTH_STEP;
+ data.width = data.width > MAX_COLUMN_WIDTH ?
+ INITIAL_COLUMN_WIDTH : data.width;
+ },
+ reflow: function () {
+ var column = [];
+ var index = 0;
+ var filteredItems =
+ data.items.filter(function (item) {
+ return item.name.toLowerCase()
+ .indexOf(data.filter.toLowerCase()) !== -1;
+ });
+
+ data.columns = [];
+
+ while (index < filteredItems.length) {
+ if (column.length >= data.rowCount) {
+ data.columns.push(column);
+ column = [];
+ }
+
+ column.push(filteredItems[index]);
+ index += 1;
+ }
+
+ if (column.length > 0) {
+ data.columns.push(column);
+ }
+ }
+ },
+ watch: {
+ filter: 'reflow',
+ items: 'reflow',
+ rowCount: 'reflow'
+ },
+ template: autoflowTemplate,
+ destroyed: function () {
+ controller.destroy();
+
+ if (interval) {
+ clearInterval(interval);
+ interval = undefined;
+ }
+ },
+ mounted: function () {
+ controller.activate();
+
+ var updateRowHeight = function () {
+ var tabularArea = this.$refs.autoflowItems;
+ var height = tabularArea ? tabularArea.clientHeight : 0;
+ var available = height - SLIDER_HEIGHT;
+ var rows = Math.max(1, Math.floor(available / ROW_HEIGHT));
+ data.rowCount = rows;
+ }.bind(this);
+
+ interval = setInterval(updateRowHeight, 50);
+ this.$nextTick(updateRowHeight);
+ }
+ });
+ }
+
+ AutoflowTabularView.prototype = Object.create(VueView.prototype);
+
+ return AutoflowTabularView;
+});
+
diff --git a/src/plugins/autoflow/autoflow-tabular.html b/src/plugins/autoflow/autoflow-tabular.html
new file mode 100644
index 0000000000..f455ac0da4
--- /dev/null
+++ b/src/plugins/autoflow/autoflow-tabular.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+ -
+ {{row.value}}
+ {{row.name}}
+
+
+
+
diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js
index 6f8cb30e57..708f84e21f 100644
--- a/src/plugins/plugins.js
+++ b/src/plugins/plugins.js
@@ -24,7 +24,7 @@ define([
'lodash',
'./utcTimeSystem/plugin',
'../../example/generator/plugin',
- '../../platform/features/autoflow/plugin',
+ './autoflow/AutoflowTabularPlugin',
'./timeConductor/plugin',
'../../example/imagery/plugin',
'../../platform/import-export/bundle',
diff --git a/src/ui/VueView.js b/src/ui/VueView.js
new file mode 100644
index 0000000000..5e7409b233
--- /dev/null
+++ b/src/ui/VueView.js
@@ -0,0 +1,33 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2017, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define(['vue'], function (Vue) {
+ function VueView(options) {
+ var vm = new Vue(options);
+ this.show = function (container) {
+ container.appendChild(vm.$mount().$el);
+ };
+ this.destroy = vm.$destroy.bind(vm);
+ }
+
+ return VueView;
+});
diff --git a/test-main.js b/test-main.js
index 6d095c25b9..82c3adcf2e 100644
--- a/test-main.js
+++ b/test-main.js
@@ -63,6 +63,7 @@ requirejs.config({
"screenfull": "bower_components/screenfull/dist/screenfull.min",
"text": "bower_components/text/text",
"uuid": "bower_components/node-uuid/uuid",
+ "vue": "node_modules/vue/dist/vue.min",
"zepto": "bower_components/zepto/zepto.min",
"lodash": "bower_components/lodash/lodash",
"d3-selection": "node_modules/d3-selection/build/d3-selection.min",