diff --git a/index.html b/index.html index fbb8b7d489..1405bcec24 100644 --- a/index.html +++ b/index.html @@ -43,6 +43,9 @@ openmct.install(openmct.plugins.ExampleImagery()); openmct.install(openmct.plugins.UTCTimeSystem()); openmct.install(openmct.plugins.ImportExport()); + openmct.install(openmct.plugins.AutoflowView({ + type: "telemetry.panel" + })); openmct.install(openmct.plugins.Conductor({ menuOptions: [ { diff --git a/karma.conf.js b/karma.conf.js index f3b52d13a1..533ebbff3d 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -36,6 +36,7 @@ module.exports = function(config) { files: [ {pattern: 'bower_components/**/*.js', included: false}, {pattern: 'node_modules/d3-*/**/*.js', included: false}, + {pattern: 'node_modules/vue/**/*.js', included: false}, {pattern: 'src/**/*.js', included: false}, {pattern: 'example/**/*.html', included: false}, {pattern: 'example/**/*.js', included: false}, diff --git a/openmct.js b/openmct.js index 57c67ca194..05d0b2adbd 100644 --- a/openmct.js +++ b/openmct.js @@ -37,6 +37,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", diff --git a/package.json b/package.json index 35f2a42261..c577073ca5 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "d3-time-format": "^2.0.3", "express": "^4.13.1", "minimist": "^1.1.1", - "request": "^2.69.0" + "request": "^2.69.0", + "vue": "^2.5.6" }, "devDependencies": { "bower": "^1.7.7", diff --git a/platform/features/autoflow/plugin.js b/platform/features/autoflow/plugin.js deleted file mode 100755 index 5d17cfbf09..0000000000 --- a/platform/features/autoflow/plugin.js +++ /dev/null @@ -1,54 +0,0 @@ -define([ - 'text!./res/templates/autoflow-tabular.html', - './src/AutoflowTabularController', - './src/MCTAutoflowTable' -], function ( - autoflowTabularTemplate, - AutoflowTabularController, - MCTAutoflowTable -) { - return function (options) { - return function (openmct) { - openmct.legacyRegistry.register("platform/features/autoflow", { - "name": "WARP Telemetry Adapter", - "description": "Retrieves telemetry from the WARP Server and provides related types and views.", - "resources": "res", - "extensions": { - "views": [ - { - "key": "autoflow", - "name": "Autoflow Tabular", - "cssClass": "icon-packet", - "description": "A tabular view of packet contents.", - "template": autoflowTabularTemplate, - "type": options && options.type, - "needs": [ - "telemetry" - ], - "delegation": true - } - ], - "controllers": [ - { - "key": "AutoflowTabularController", - "implementation": AutoflowTabularController, - "depends": [ - "$scope", - "$timeout", - "telemetrySubscriber" - ] - } - ], - "directives": [ - { - "key": "mctAutoflowTable", - "implementation": MCTAutoflowTable - } - ] - } - }); - openmct.legacyRegistry.enable("platform/features/autoflow"); - }; - }; -}); - diff --git a/platform/features/autoflow/res/templates/autoflow-tabular.html b/platform/features/autoflow/res/templates/autoflow-tabular.html deleted file mode 100755 index 59e181fc48..0000000000 --- a/platform/features/autoflow/res/templates/autoflow-tabular.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
- - -
{{autoflow.updated()}}
- -
-
- - -
-
diff --git a/platform/features/autoflow/src/AutoflowTableLinker.js b/platform/features/autoflow/src/AutoflowTableLinker.js deleted file mode 100755 index 0c9e5da5fd..0000000000 --- a/platform/features/autoflow/src/AutoflowTableLinker.js +++ /dev/null @@ -1,169 +0,0 @@ -/*global angular*/ -define( - [], - function () { - - /** - * The link step for the `mct-autoflow-table` directive; - * watches scope and updates the DOM appropriately. - * See documentation in `MCTAutoflowTable.js` for the rationale - * for including this directive, as well as for an explanation - * of which values are placed in scope. - * - * @constructor - * @param {Scope} scope the scope for this usage of the directive - * @param element the jqLite-wrapped element which used this directive - */ - function AutoflowTableLinker(scope, element) { - var objects, // Domain objects at last structure refresh - rows, // Number of rows from last structure refresh - priorClasses = {}, - valueSpans = {}; // Span elements to put data values in - - // Create a new name-value pair in the specified column - function createListItem(domainObject, ul) { - // Create a new li, and spans to go in it. - var li = angular.element('
  • '), - titleSpan = angular.element(''), - valueSpan = angular.element(''); - - // Place spans in the li, and li into the column. - // valueSpan must precede titleSpan in the DOM due to new CSS float approach - li.append(valueSpan).append(titleSpan); - ul.append(li); - - // Style appropriately - li.addClass('l-autoflow-row'); - titleSpan.addClass('l-autoflow-item l'); - valueSpan.addClass('l-autoflow-item r l-obj-val-format'); - - // Set text/tooltip for the name-value row - titleSpan.text(domainObject.getModel().name); - titleSpan.attr("title", domainObject.getModel().name); - - // Keep a reference to the span which will hold the - // data value, to populate in the next refreshValues call - valueSpans[domainObject.getId()] = valueSpan; - - return li; - } - - // Create a new column of name-value pairs in this table. - function createColumn(el) { - // Create a ul - var ul = angular.element('
      '); - - // Add it into the mct-autoflow-table - el.append(ul); - - // Style appropriately - ul.addClass('l-autoflow-col'); - - // Get the current col width and apply at time of column creation - // Important to do this here, as new columns could be created after - // the user has changed the width. - ul.css('width', scope.columnWidth + 'px'); - - // Return it, so some li elements can be added - return ul; - } - - // Change the width of the columns when user clicks the resize button. - function resizeColumn() { - element.find('ul').css('width', scope.columnWidth + 'px'); - } - - // Rebuild the DOM associated with this table. - function rebuild(domainObjects, rowCount) { - var activeColumn; - - // Empty out our cached span elements - valueSpans = {}; - - // Start with an empty DOM beneath this directive - element.html(""); - - // Add DOM elements for each domain object being displayed - // in this table. - domainObjects.forEach(function (object, index) { - // Start a new column if we'd run out of room - if (index % rowCount === 0) { - activeColumn = createColumn(element); - } - // Add the DOM elements for that object to whichever - // column (a `ul` element) is current. - createListItem(object, activeColumn); - }); - } - - // Update spans with values, as made available via the - // `values` attribute of this directive. - function refreshValues() { - // Get the available values - var values = scope.values || {}, - classes = scope.classes || {}; - - // Populate all spans with those values (or clear - // those spans if no value is available) - (objects || []).forEach(function (object) { - var id = object.getId(), - span = valueSpans[id], - value; - - if (span) { - // Look up the value... - value = values[id]; - // ...and convert to empty string if it's undefined - value = value === undefined ? "" : value; - span.attr("data-value", value); - - // Update the span - span.text(value); - span.attr("title", value); - span.removeClass(priorClasses[id]); - span.addClass(classes[id]); - priorClasses[id] = classes[id]; - } - // Also need stale/alert/ok class - // on span - }); - } - - // Refresh the DOM for this table, if necessary - function refreshStructure() { - // Only rebuild if number of rows or set of objects - // has changed; otherwise, our structure is still valid. - if (scope.objects !== objects || - scope.rows !== rows) { - - // Track those values to support future refresh checks - objects = scope.objects; - rows = scope.rows; - - // Rebuild the DOM - rebuild(objects || [], rows || 1); - - // Refresh all data values shown - refreshValues(); - } - } - - // Changing the domain objects in use or the number - // of rows should trigger a structure change (DOM rebuild) - scope.$watch("objects", refreshStructure); - scope.$watch("rows", refreshStructure); - - // When the current column width has been changed, resize the column - scope.$watch('columnWidth', resizeColumn); - - // When the last-updated time ticks, - scope.$watch("updated", refreshValues); - - // Update displayed values when the counter changes. - scope.$watch("counter", refreshValues); - - } - - return AutoflowTableLinker; - } -); diff --git a/platform/features/autoflow/src/AutoflowTabularController.js b/platform/features/autoflow/src/AutoflowTabularController.js deleted file mode 100755 index 3d6901f5a7..0000000000 --- a/platform/features/autoflow/src/AutoflowTabularController.js +++ /dev/null @@ -1,324 +0,0 @@ - -define( - ['moment'], - function (moment) { - - var ROW_HEIGHT = 16, - SLIDER_HEIGHT = 10, - INITIAL_COLUMN_WIDTH = 225, - MAX_COLUMN_WIDTH = 525, - COLUMN_WIDTH_STEP = 25, - DEBOUNCE_INTERVAL = 100, - DATE_FORMAT = "YYYY-DDD HH:mm:ss.SSS\\Z", - NOT_UPDATED = "No updates", - EMPTY_ARRAY = []; - - /** - * Responsible for supporting the autoflow tabular view. - * Implements the all-over logic which drives that view, - * mediating between template-provided areas, the included - * `mct-autoflow-table` directive, and the underlying - * domain object model. - * @constructor - */ - function AutflowTabularController( - $scope, - $timeout, - telemetrySubscriber - ) { - var filterValue = "", - filterValueLowercase = "", - subscription, - filteredObjects = [], - lastUpdated = {}, - updateText = NOT_UPDATED, - rangeValues = {}, - classes = {}, - limits = {}, - updatePending = false, - lastBounce = Number.NEGATIVE_INFINITY, - columnWidth = INITIAL_COLUMN_WIDTH, - rows = 1, - counter = 0; - - // Trigger an update of the displayed table by incrementing - // the counter that it watches. - function triggerDisplayUpdate() { - counter += 1; - } - - // Check whether or not an object's name matches the - // user-entered filter value. - function filterObject(domainObject) { - return (domainObject.getModel().name || "") - .toLowerCase() - .indexOf(filterValueLowercase) !== -1; - } - - // Comparator for sorting points back into packet order - function compareObject(objectA, objectB) { - var indexA = objectA.getModel().index || 0, - indexB = objectB.getModel().index || 0; - return indexA - indexB; - } - - // Update the list of currently-displayed objects; these - // will be the subset of currently subscribed-to objects - // which match a user-entered filter. - function doUpdateFilteredObjects() { - // Generate the list - filteredObjects = ( - subscription ? - subscription.getTelemetryObjects() : - [] - ).filter(filterObject).sort(compareObject); - - // Clear the pending flag - updatePending = false; - - // Track when this occurred, so that we can wait - // a whole before updating again. - lastBounce = Date.now(); - - triggerDisplayUpdate(); - } - - // Request an update to the list of current objects; this may - // run on a timeout to avoid excessive calls, e.g. while the user - // is typing a filter. - function updateFilteredObjects() { - // Don't do anything if an update is already scheduled - if (!updatePending) { - if (Date.now() > lastBounce + DEBOUNCE_INTERVAL) { - // Update immediately if it's been long enough - doUpdateFilteredObjects(); - } else { - // Otherwise, update later, and track that we have - // an update pending so that subsequent calls can - // be ignored. - updatePending = true; - $timeout(doUpdateFilteredObjects, DEBOUNCE_INTERVAL); - } - } - } - - // Track the latest data values for this domain object - function recordData(telemetryObject) { - // Get latest domain/range values for this object. - var id = telemetryObject.getId(), - domainValue = subscription.getDomainValue(telemetryObject), - rangeValue = subscription.getRangeValue(telemetryObject); - - // Track the most recent timestamp change observed... - if (domainValue !== undefined && domainValue !== lastUpdated[id]) { - lastUpdated[id] = domainValue; - // ... and update the displayable text for that timestamp - updateText = isNaN(domainValue) ? "" : - moment.utc(domainValue).format(DATE_FORMAT); - } - - // Store data values into the rangeValues structure, which - // will be used to populate the table itself. - // Note that we want full precision here. - rangeValues[id] = rangeValue; - - // Update limit states as well - classes[id] = limits[id] && (limits[id].evaluate({ - // This relies on external knowledge that the - // range value of a telemetry point is encoded - // in its datum as "value." - value: rangeValue - }) || {}).cssClass; - } - - - // Look at telemetry objects from the subscription; this is watched - // to detect changes from the subscription. - function subscribedTelemetry() { - return subscription ? - subscription.getTelemetryObjects() : EMPTY_ARRAY; - } - - // Update the data values which will be used to populate the table - function updateValues() { - subscribedTelemetry().forEach(recordData); - triggerDisplayUpdate(); - } - - // Getter-setter function for user-entered filter text. - function filter(value) { - // If value was specified, we're a setter - if (value !== undefined) { - // Store the new value - filterValue = value; - filterValueLowercase = value.toLowerCase(); - // Change which objects appear in the table - updateFilteredObjects(); - } - - // Always act as a getter - return filterValue; - } - - // Update the bounds (width and height) of this view; - // called from the mct-resize directive. Recalculates how - // many rows should appear in the contained table. - function setBounds(bounds) { - var availableSpace = bounds.height - SLIDER_HEIGHT; - rows = Math.max(1, Math.floor(availableSpace / ROW_HEIGHT)); - } - - // Increment the current column width, up to the defined maximum. - // When the max is hit, roll back to the default. - function increaseColumnWidth() { - columnWidth += COLUMN_WIDTH_STEP; - // Cycle down to the initial width instead of exceeding max - columnWidth = columnWidth > MAX_COLUMN_WIDTH ? - INITIAL_COLUMN_WIDTH : columnWidth; - } - - // Get displayable text for last-updated value - function updated() { - return updateText; - } - - // Unsubscribe, if a subscription is active. - function releaseSubscription() { - if (subscription) { - subscription.unsubscribe(); - subscription = undefined; - } - } - - // Update set of telemetry objects managed by this view - function updateTelemetryObjects(telemetryObjects) { - updateFilteredObjects(); - limits = {}; - telemetryObjects.forEach(function (telemetryObject) { - var id = telemetryObject.getId(); - limits[id] = telemetryObject.getCapability('limit'); - }); - } - - // Create a subscription for the represented domain object. - // This will resolve capability delegation as necessary. - function makeSubscription(domainObject) { - // Unsubscribe, if there is an existing subscription - releaseSubscription(); - - // Clear updated timestamp - lastUpdated = {}; - updateText = NOT_UPDATED; - - // Create a new subscription; telemetrySubscriber gets - // to do the meaningful work here. - subscription = domainObject && telemetrySubscriber.subscribe( - domainObject, - updateValues - ); - - // Our set of in-view telemetry objects may have changed, - // so update the set that is being passed down to the table. - updateFilteredObjects(); - } - - // Watch for changes to the set of objects which have telemetry - $scope.$watch(subscribedTelemetry, updateTelemetryObjects); - - // Watch for the represented domainObject (this field will - // be populated by mct-representation) - $scope.$watch("domainObject", makeSubscription); - - // Make sure we unsubscribe when this view is destroyed. - $scope.$on("$destroy", releaseSubscription); - - return { - /** - * Get the number of rows which should be shown in this table. - * @return {number} the number of rows to show - */ - getRows: function () { - return rows; - }, - /** - * Get the objects which should currently be displayed in - * this table. This will be watched, so the return value - * should be stable when this list is unchanging. Only - * objects which match the user-entered filter value should - * be returned here. - * @return {DomainObject[]} the domain objects to include in - * this table. - */ - getTelemetryObjects: function () { - return filteredObjects; - }, - /** - * Set the bounds (width/height) of this autoflow tabular view. - * The template must ensure that these bounds are tracked on - * the table area only. - * @param bounds the bounds; and object with `width` and - * `height` properties, both as numbers, in pixels. - */ - setBounds: setBounds, - /** - * Increments the width of the autoflow column. - * Setting does not yet persist. - */ - increaseColumnWidth: increaseColumnWidth, - /** - * Get-or-set the user-supplied filter value. - * @param {string} [value] the new filter value; omit to use - * as a getter - * @returns {string} the user-supplied filter value - */ - filter: filter, - /** - * Get all range values for use in this table. These will be - * returned as an object of key-value pairs, where keys are - * domain object IDs, and values are the most recently observed - * data values associated with those objects, formatted for - * display. - * @returns {object.} most recent values - */ - rangeValues: function () { - return rangeValues; - }, - /** - * Get CSS classes to apply to specific rows, representing limit - * states and/or stale states. These are returned as key-value - * pairs where keys are domain object IDs, and values are CSS - * classes to display for domain objects with those IDs. - * @returns {object.} CSS classes - */ - classes: function () { - return classes; - }, - /** - * Get the "last updated" text for this view; this will be - * the most recent timestamp observed for any telemetry- - * providing object, formatted for display. - * @returns {string} the time of the most recent update - */ - updated: updated, - /** - * Get the current column width, in pixels. - * @returns {number} column width - */ - columnWidth: function () { - return columnWidth; - }, - /** - * Keep a counter and increment this whenever the display - * should be updated; this will be watched by the - * `mct-autoflow-table`. - * @returns {number} a counter value - */ - counter: function () { - return counter; - } - }; - } - - return AutflowTabularController; - } -); diff --git a/platform/features/autoflow/src/MCTAutoflowTable.js b/platform/features/autoflow/src/MCTAutoflowTable.js deleted file mode 100755 index dc5c24f42c..0000000000 --- a/platform/features/autoflow/src/MCTAutoflowTable.js +++ /dev/null @@ -1,60 +0,0 @@ - -define( - ["./AutoflowTableLinker"], - function (AutoflowTableLinker) { - - /** - * The `mct-autoflow-table` directive specifically supports - * autoflow tabular views; it is not intended for use outside - * of that view. - * - * This directive is responsible for creating the structure - * of the table in this view, and for updating its values. - * While this is achievable using a regular Angular template, - * this is undesirable from the perspective of performance - * due to the number of watches that can be involved for large - * tables. Instead, this directive will maintain a small number - * of watches, rebuilding table structure only when necessary, - * and updating displayed values in the more common case of - * new data arriving. - * - * @constructor - */ - function MCTAutoflowTable() { - return { - // Only applicable at the element level - restrict: "E", - - // The link function; handles DOM update/manipulation - link: AutoflowTableLinker, - - // Parameters to pass from attributes into scope - scope: { - // Set of domain objects to show in the table - objects: "=", - - // Values for those objects, by ID - values: "=", - - // CSS classes to show for objects, by ID - classes: "=", - - // Number of rows to show before autoflowing - rows: "=", - - // Time of last update; watched to refresh values - updated: "=", - - // Current width of the autoflow column - columnWidth: "=", - - // A counter used to trigger display updates - counter: "=" - } - }; - } - - return MCTAutoflowTable; - - } -); diff --git a/platform/features/autoflow/test/AutoflowTableLinkerSpec.js b/platform/features/autoflow/test/AutoflowTableLinkerSpec.js deleted file mode 100755 index 0ae08db4d8..0000000000 --- a/platform/features/autoflow/test/AutoflowTableLinkerSpec.js +++ /dev/null @@ -1,178 +0,0 @@ - -define( - ["../src/AutoflowTableLinker"], - function (AutoflowTableLinker) { - - describe("The mct-autoflow-table linker", function () { - var cachedAngular, - mockAngular, - mockScope, - mockElement, - mockElements, - linker; - - // Utility function to generate more mock elements - function createMockElement(html) { - var mockEl = jasmine.createSpyObj( - "element-" + html, - [ - "append", - "addClass", - "removeClass", - "text", - "attr", - "html", - "css", - "find" - ] - ); - mockEl.testHtml = html; - mockEl.append.andReturn(mockEl); - mockElements.push(mockEl); - return mockEl; - } - - function createMockDomainObject(id) { - var mockDomainObject = jasmine.createSpyObj( - "domainObject-" + id, - ["getId", "getModel"] - ); - mockDomainObject.getId.andReturn(id); - mockDomainObject.getModel.andReturn({name: id.toUpperCase()}); - return mockDomainObject; - } - - function fireWatch(watchExpression, value) { - mockScope.$watch.calls.forEach(function (call) { - if (call.args[0] === watchExpression) { - call.args[1](value); - } - }); - } - - // AutoflowTableLinker accesses Angular in the global - // scope, since it is not injectable; we simulate that - // here by adding/removing it to/from the window object. - beforeEach(function () { - mockElements = []; - - mockAngular = jasmine.createSpyObj("angular", ["element"]); - mockScope = jasmine.createSpyObj("scope", ["$watch"]); - mockElement = createMockElement('
      '); - - mockAngular.element.andCallFake(createMockElement); - - if (window.angular !== undefined) { - cachedAngular = window.angular; - } - window.angular = mockAngular; - - linker = new AutoflowTableLinker(mockScope, mockElement); - }); - - afterEach(function () { - if (cachedAngular !== undefined) { - window.angular = cachedAngular; - } else { - delete window.angular; - } - }); - - it("watches for changes in inputs", function () { - expect(mockScope.$watch).toHaveBeenCalledWith( - "objects", - jasmine.any(Function) - ); - expect(mockScope.$watch).toHaveBeenCalledWith( - "rows", - jasmine.any(Function) - ); - expect(mockScope.$watch).toHaveBeenCalledWith( - "counter", - jasmine.any(Function) - ); - }); - - it("changes structure when domain objects change", function () { - // Set up scope - mockScope.rows = 4; - mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f'] - .map(createMockDomainObject); - - // Fire an update to the set of objects - fireWatch("objects"); - - // Should have rebuilt with two columns of - // four and two rows each; first, by clearing... - expect(mockElement.html).toHaveBeenCalledWith(""); - - // Should have appended two columns... - expect(mockElement.append.calls.length).toEqual(2); - - // ...which should have received two and four rows each - expect(mockElement.append.calls[0].args[0].append.calls.length) - .toEqual(4); - expect(mockElement.append.calls[1].args[0].append.calls.length) - .toEqual(2); - }); - - it("updates values", function () { - var mockSpans; - - mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f'] - .map(createMockDomainObject); - mockScope.values = { a: 0 }; - - // Fire an update to the set of values - fireWatch("objects"); - fireWatch("updated"); - - // Get all created spans - mockSpans = mockElements.filter(function (mockElem) { - return mockElem.testHtml === ''; - }); - - // First span should be a, should have gotten this value. - // This test detects, in particular, WTD-749 - expect(mockSpans[0].text).toHaveBeenCalledWith('A'); - expect(mockSpans[1].text).toHaveBeenCalledWith(0); - }); - - it("listens for changes in column width", function () { - var mockUL = createMockElement("
        "); - mockElement.find.andReturn(mockUL); - mockScope.columnWidth = 200; - fireWatch("columnWidth", mockScope.columnWidth); - expect(mockUL.css).toHaveBeenCalledWith("width", "200px"); - }); - - it("updates CSS classes", function () { - var mockSpans; - - mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f'] - .map(createMockDomainObject); - mockScope.values = { a: "a value to find" }; - mockScope.classes = { a: 'class-a' }; - - // Fire an update to the set of values - fireWatch("objects"); - fireWatch("updated"); - - // Figure out which span holds the relevant value... - mockSpans = mockElements.filter(function (mockElem) { - return mockElem.testHtml === ''; - }).filter(function (mockSpan) { - var attrCalls = mockSpan.attr.calls; - return attrCalls.some(function (call) { - return call.args[0] === 'title' && - call.args[1] === mockScope.values.a; - }); - }); - - // ...and make sure it also has had its class applied - expect(mockSpans[0].addClass) - .toHaveBeenCalledWith(mockScope.classes.a); - }); - }); - } -); diff --git a/platform/features/autoflow/test/AutoflowTabularControllerSpec.js b/platform/features/autoflow/test/AutoflowTabularControllerSpec.js deleted file mode 100755 index 14edafab69..0000000000 --- a/platform/features/autoflow/test/AutoflowTabularControllerSpec.js +++ /dev/null @@ -1,341 +0,0 @@ - -define( - ["../src/AutoflowTabularController"], - function (AutoflowTabularController) { - - describe("The autoflow tabular controller", function () { - var mockScope, - mockTimeout, - mockSubscriber, - mockDomainObject, - mockSubscription, - controller; - - // Fire watches that are registered as functions. - function fireFnWatches() { - mockScope.$watch.calls.forEach(function (call) { - if (typeof call.args[0] === 'function') { - call.args[1](call.args[0]()); - } - }); - } - - beforeEach(function () { - mockScope = jasmine.createSpyObj( - "$scope", - ["$on", "$watch"] - ); - mockTimeout = jasmine.createSpy("$timeout"); - mockSubscriber = jasmine.createSpyObj( - "telemetrySubscriber", - ["subscribe"] - ); - mockDomainObject = jasmine.createSpyObj( - "domainObject", - ["getId", "getModel", "getCapability"] - ); - mockSubscription = jasmine.createSpyObj( - "subscription", - [ - "unsubscribe", - "getTelemetryObjects", - "getDomainValue", - "getRangeValue" - ] - ); - - mockSubscriber.subscribe.andReturn(mockSubscription); - mockDomainObject.getModel.andReturn({name: "something"}); - - controller = new AutoflowTabularController( - mockScope, - mockTimeout, - mockSubscriber - ); - }); - - it("listens for the represented domain object", function () { - expect(mockScope.$watch).toHaveBeenCalledWith( - "domainObject", - jasmine.any(Function) - ); - }); - - it("provides a getter-setter function for filtering", function () { - expect(controller.filter()).toEqual(""); - controller.filter("something"); - expect(controller.filter()).toEqual("something"); - }); - - it("tracks bounds and adjust number of rows accordingly", function () { - // Rows are 15px high, and need room for an 10px slider - controller.setBounds({ width: 700, height: 120 }); - expect(controller.getRows()).toEqual(6); // 110 usable height / 16px - controller.setBounds({ width: 700, height: 240 }); - expect(controller.getRows()).toEqual(14); // 230 usable height / 16px - }); - - it("subscribes to a represented object's telemetry", function () { - // Set up subscription, scope - mockSubscription.getTelemetryObjects - .andReturn([mockDomainObject]); - mockScope.domainObject = mockDomainObject; - - // Invoke the watcher with represented domain object - mockScope.$watch.mostRecentCall.args[1](mockDomainObject); - - // Should have subscribed to it - expect(mockSubscriber.subscribe).toHaveBeenCalledWith( - mockDomainObject, - jasmine.any(Function) - ); - - // Should report objects as reported from subscription - expect(controller.getTelemetryObjects()) - .toEqual([mockDomainObject]); - }); - - it("releases subscriptions on destroy", function () { - // Set up subscription... - mockSubscription.getTelemetryObjects - .andReturn([mockDomainObject]); - mockScope.domainObject = mockDomainObject; - mockScope.$watch.mostRecentCall.args[1](mockDomainObject); - - // Verify precondition - expect(mockSubscription.unsubscribe).not.toHaveBeenCalled(); - - // Make sure we're listening for $destroy - expect(mockScope.$on).toHaveBeenCalledWith( - "$destroy", - jasmine.any(Function) - ); - - // Fire a destroy event - mockScope.$on.mostRecentCall.args[1](); - - // Should have unsubscribed - expect(mockSubscription.unsubscribe).toHaveBeenCalled(); - }); - - it("presents latest values and latest update state", function () { - // Make sure values are available - mockSubscription.getDomainValue.andReturn(402654321123); - mockSubscription.getRangeValue.andReturn(789); - mockDomainObject.getId.andReturn('testId'); - - // Set up subscription... - mockSubscription.getTelemetryObjects - .andReturn([mockDomainObject]); - mockScope.domainObject = mockDomainObject; - mockScope.$watch.mostRecentCall.args[1](mockDomainObject); - - // Fire subscription callback - mockSubscriber.subscribe.mostRecentCall.args[1](); - - // ...and exposed the results for template to consume - expect(controller.updated()).toEqual("1982-278 08:25:21.123Z"); - expect(controller.rangeValues().testId).toEqual(789); - }); - - it("sorts domain objects by index", function () { - var testIndexes = { a: 2, b: 1, c: 3, d: 0 }, - mockDomainObjects = Object.keys(testIndexes).sort().map(function (id) { - var mockDomainObj = jasmine.createSpyObj( - "domainObject", - ["getId", "getModel"] - ); - - mockDomainObj.getId.andReturn(id); - mockDomainObj.getModel.andReturn({ index: testIndexes[id] }); - - return mockDomainObj; - }); - - // Expose those domain objects... - mockSubscription.getTelemetryObjects.andReturn(mockDomainObjects); - mockScope.domainObject = mockDomainObject; - mockScope.$watch.mostRecentCall.args[1](mockDomainObject); - - // Fire subscription callback - mockSubscriber.subscribe.mostRecentCall.args[1](); - - // Controller should expose same objects, but sorted by index from model - expect(controller.getTelemetryObjects()).toEqual([ - mockDomainObjects[3], // d, index=0 - mockDomainObjects[1], // b, index=1 - mockDomainObjects[0], // a, index=2 - mockDomainObjects[2] // c, index=3 - ]); - }); - - it("uses a timeout to throttle update", function () { - // Set up subscription... - mockSubscription.getTelemetryObjects - .andReturn([mockDomainObject]); - mockScope.domainObject = mockDomainObject; - - // Set the object in view; should not need a timeout - mockScope.$watch.mostRecentCall.args[1](mockDomainObject); - expect(mockTimeout.calls.length).toEqual(0); - - // Next call should schedule an update on a timeout - mockScope.$watch.mostRecentCall.args[1](mockDomainObject); - expect(mockTimeout.calls.length).toEqual(1); - - // ...but this last one should not, since existing - // timeout will cover it - mockScope.$watch.mostRecentCall.args[1](mockDomainObject); - expect(mockTimeout.calls.length).toEqual(1); - }); - - it("allows changing column width", function () { - var initialWidth = controller.columnWidth(); - controller.increaseColumnWidth(); - expect(controller.columnWidth()).toBeGreaterThan(initialWidth); - }); - - describe("filter", function () { - var doFilter, - filteredObjects, - filteredObjectNames; - - beforeEach(function () { - var telemetryObjects, - updateFilteredObjects; - - telemetryObjects = [ - 'DEF123', - 'abc789', - '456abc', - '4ab3cdef', - 'hjs[12].*(){}^\\' - ].map(function (objectName, index) { - var mockTelemetryObject = jasmine.createSpyObj( - objectName, - ["getId", "getModel"] - ); - - mockTelemetryObject.getId.andReturn(objectName); - mockTelemetryObject.getModel.andReturn({ - name: objectName, - index: index - }); - - return mockTelemetryObject; - }); - - mockSubscription - .getTelemetryObjects - .andReturn(telemetryObjects); - - // Trigger domainObject change to create subscription. - mockScope.$watch.mostRecentCall.args[1](mockDomainObject); - - updateFilteredObjects = function () { - filteredObjects = controller.getTelemetryObjects(); - filteredObjectNames = filteredObjects.map(function (o) { - return o.getModel().name; - }); - }; - - doFilter = function (term) { - controller.filter(term); - // Filter is debounced so we have to force it to occur. - mockTimeout.mostRecentCall.args[0](); - updateFilteredObjects(); - }; - - updateFilteredObjects(); - }); - - it("initially shows all objects", function () { - expect(filteredObjectNames).toEqual([ - 'DEF123', - 'abc789', - '456abc', - '4ab3cdef', - 'hjs[12].*(){}^\\' - ]); - }); - - it("by blank string matches all objects", function () { - doFilter(''); - expect(filteredObjectNames).toEqual([ - 'DEF123', - 'abc789', - '456abc', - '4ab3cdef', - 'hjs[12].*(){}^\\' - ]); - }); - - it("exactly matches an object name", function () { - doFilter('4ab3cdef'); - expect(filteredObjectNames).toEqual(['4ab3cdef']); - }); - - it("partially matches object names", function () { - doFilter('abc'); - expect(filteredObjectNames).toEqual([ - 'abc789', - '456abc' - ]); - }); - - it("matches case insensitive names", function () { - doFilter('def'); - expect(filteredObjectNames).toEqual([ - 'DEF123', - '4ab3cdef' - ]); - }); - - it("works as expected with special characters", function () { - doFilter('[12]'); - expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']); - doFilter('.*'); - expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']); - doFilter('.*()'); - expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']); - doFilter('.*?'); - expect(filteredObjectNames).toEqual([]); - doFilter('.+'); - expect(filteredObjectNames).toEqual([]); - }); - - it("exposes CSS classes from limits", function () { - var id = mockDomainObject.getId(), - testClass = "some-css-class", - mockLimitCapability = - jasmine.createSpyObj('limit', ['evaluate']); - - mockDomainObject.getCapability.andCallFake(function (key) { - return key === 'limit' && mockLimitCapability; - }); - mockLimitCapability.evaluate - .andReturn({ cssClass: testClass }); - - mockSubscription.getTelemetryObjects - .andReturn([mockDomainObject]); - - fireFnWatches(); - mockSubscriber.subscribe.mostRecentCall.args[1](); - - expect(controller.classes()[id]).toEqual(testClass); - }); - - it("exposes a counter that changes with each update", function () { - var i, prior; - - for (i = 0; i < 10; i += 1) { - prior = controller.counter(); - expect(controller.counter()).toEqual(prior); - mockSubscriber.subscribe.mostRecentCall.args[1](); - expect(controller.counter()).not.toEqual(prior); - } - }); - }); - }); - } -); diff --git a/platform/features/autoflow/test/MCTAutoflowTableSpec.js b/platform/features/autoflow/test/MCTAutoflowTableSpec.js deleted file mode 100755 index d86777b9b5..0000000000 --- a/platform/features/autoflow/test/MCTAutoflowTableSpec.js +++ /dev/null @@ -1,39 +0,0 @@ - -define( - ["../src/MCTAutoflowTable"], - function (MCTAutoflowTable) { - - describe("The mct-autoflow-table directive", function () { - var mctAutoflowTable; - - beforeEach(function () { - mctAutoflowTable = new MCTAutoflowTable(); - }); - - // Real functionality is contained/tested in the linker, - // so just check to make sure we're exposing the directive - // appropriately. - it("is applicable at the element level", function () { - expect(mctAutoflowTable.restrict).toEqual("E"); - }); - - it("two-ways binds needed scope variables", function () { - expect(mctAutoflowTable.scope).toEqual({ - objects: "=", - values: "=", - rows: "=", - updated: "=", - classes: "=", - columnWidth: "=", - counter: "=" - }); - }); - - it("provides a link function", function () { - expect(mctAutoflowTable.link).toEqual(jasmine.any(Function)); - }); - - - }); - } -); diff --git a/src/plugins/autoflow/AutoflowTabularConstants.js b/src/plugins/autoflow/AutoflowTabularConstants.js new file mode 100644 index 0000000000..6c69463009 --- /dev/null +++ b/src/plugins/autoflow/AutoflowTabularConstants.js @@ -0,0 +1,34 @@ +/***************************************************************************** + * 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 () { + /** + * Constant values used by the Autoflow Tabular View. + */ + return { + ROW_HEIGHT: 16, + SLIDER_HEIGHT: 10, + INITIAL_COLUMN_WIDTH: 225, + MAX_COLUMN_WIDTH: 525, + COLUMN_WIDTH_STEP: 25 + }; +}); diff --git a/src/plugins/autoflow/AutoflowTabularController.js b/src/plugins/autoflow/AutoflowTabularController.js new file mode 100644 index 0000000000..1938d664a4 --- /dev/null +++ b/src/plugins/autoflow/AutoflowTabularController.js @@ -0,0 +1,121 @@ +/***************************************************************************** + * 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([ + './AutoflowTabularRowController' +], function (AutoflowTabularRowController) { + /** + * Controller for an Autoflow Tabular View. Subscribes to telemetry + * associated with children of the domain object and passes that + * information on to the view. + * + * @param {DomainObject} domainObject the object being viewed + * @param {*} data the view data + * @param openmct a reference to the openmct application + */ + function AutoflowTabularController(domainObject, data, openmct) { + this.composition = openmct.composition.get(domainObject); + this.data = data; + this.openmct = openmct; + + this.rows = {}; + this.controllers = {}; + + this.addRow = this.addRow.bind(this); + this.removeRow = this.removeRow.bind(this); + } + + /** + * Set the "Last Updated" value to be displayed. + * @param {String} value the value to display + * @private + */ + AutoflowTabularController.prototype.trackLastUpdated = function (value) { + this.data.updated = value; + }; + + /** + * Respond to an `add` event from composition by adding a new row. + * @private + */ + AutoflowTabularController.prototype.addRow = function (childObject) { + var identifier = childObject.identifier; + var id = [identifier.namespace, identifier.key].join(":"); + + if (!this.rows[id]) { + this.rows[id] = { + classes: "", + name: childObject.name, + value: undefined + }; + this.controllers[id] = new AutoflowTabularRowController( + childObject, + this.rows[id], + this.openmct, + this.trackLastUpdated.bind(this) + ); + this.controllers[id].activate(); + this.data.items.push(this.rows[id]); + } + }; + + /** + * Respond to an `remove` event from composition by removing any + * related row. + * @private + */ + AutoflowTabularController.prototype.removeRow = function (identifier) { + var id = [identifier.namespace, identifier.key].join(":"); + + if (this.rows[id]) { + this.data.items = this.data.items.filter(function (item) { + return item !== this.rows[id]; + }.bind(this)); + this.controllers[id].destroy(); + delete this.controllers[id]; + delete this.rows[id]; + } + }; + + /** + * Activate this controller; begin listening for changes. + */ + AutoflowTabularController.prototype.activate = function () { + this.composition.on('add', this.addRow); + this.composition.on('remove', this.removeRow); + this.composition.load(); + }; + + /** + * Destroy this controller; detach any associated resources. + */ + AutoflowTabularController.prototype.destroy = function () { + Object.keys(this.controllers).forEach(function (id) { + this.controllers[id].destroy(); + }.bind(this)); + this.controllers = {}; + this.composition.off('add', this.addRow); + this.composition.off('remove', this.removeRow); + }; + + return AutoflowTabularController; +}); diff --git a/src/plugins/autoflow/AutoflowTabularPlugin.js b/src/plugins/autoflow/AutoflowTabularPlugin.js new file mode 100644 index 0000000000..fac41ba359 --- /dev/null +++ b/src/plugins/autoflow/AutoflowTabularPlugin.js @@ -0,0 +1,60 @@ +/***************************************************************************** + * 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([ + './AutoflowTabularView' +], function ( + AutoflowTabularView +) { + /** + * This plugin provides an Autoflow Tabular View for domain objects + * in Open MCT. + * + * @param {Object} options + * @param {String} [options.type] the domain object type for which + * this view should be available; if omitted, this view will + * be available for all objects + */ + return function (options) { + return function (openmct) { + var views = (openmct.mainViews || openmct.objectViews); + + views.addProvider({ + name: "Autoflow Tabular", + key: "autoflow", + cssClass: "icon-packet", + description: "A tabular view of packet contents.", + canView: function (d) { + return !options || (options.type === d.type); + }, + view: function (domainObject) { + return new AutoflowTabularView( + domainObject, + openmct, + document + ); + } + }); + }; + }; +}); + diff --git a/src/plugins/autoflow/AutoflowTabularPluginSpec.js b/src/plugins/autoflow/AutoflowTabularPluginSpec.js new file mode 100644 index 0000000000..01356c2c7e --- /dev/null +++ b/src/plugins/autoflow/AutoflowTabularPluginSpec.js @@ -0,0 +1,319 @@ +/***************************************************************************** + * 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([ + './AutoflowTabularPlugin', + './AutoflowTabularConstants', + '../../MCT', + 'zepto' +], function (AutoflowTabularPlugin, AutoflowTabularConstants, MCT, $) { + describe("AutoflowTabularPlugin", function () { + var testType; + var testObject; + var mockmct; + + beforeEach(function () { + testType = "some-type"; + testObject = { type: testType }; + mockmct = new MCT(); + spyOn(mockmct.composition, 'get'); + spyOn(mockmct.objectViews, 'addProvider'); + spyOn(mockmct.telemetry, 'getMetadata'); + spyOn(mockmct.telemetry, 'getValueFormatter'); + spyOn(mockmct.telemetry, 'limitEvaluator'); + spyOn(mockmct.telemetry, 'request'); + spyOn(mockmct.telemetry, 'subscribe'); + + var plugin = new AutoflowTabularPlugin({ type: testType }); + plugin(mockmct); + }); + + it("installs a view provider", function () { + expect(mockmct.objectViews.addProvider).toHaveBeenCalled(); + }); + + describe("installs a view provider which", function () { + var provider; + + beforeEach(function () { + provider = + mockmct.objectViews.addProvider.mostRecentCall.args[0]; + }); + + it("applies its view to the type from options", function () { + expect(provider.canView(testObject)).toBe(true); + }); + + it("does not apply to other types", function () { + expect(provider.canView({ type: 'foo' })).toBe(false); + }); + + describe("provides a view which", function () { + var testKeys; + var testChildren; + var testContainer; + var testHistories; + var mockComposition; + var mockMetadata; + var mockEvaluator; + var mockUnsubscribes; + var callbacks; + var view; + + function waitsForChange() { + var callback = jasmine.createSpy('callback'); + window.requestAnimationFrame(callback); + waitsFor(function () { + return callback.calls.length > 0; + }); + } + + function emitEvent(mockEmitter, type, event) { + mockEmitter.on.calls.forEach(function (call) { + if (call.args[0] === type) { + call.args[1](event); + } + }); + } + + beforeEach(function () { + callbacks = {}; + + testObject = { type: 'some-type' }; + testKeys = ['abc', 'def', 'xyz']; + testChildren = testKeys.map(function (key) { + return { + identifier: { namespace: "test", key: key }, + name: "Object " + key + }; + }); + testContainer = $('
        ')[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 @@ + +
        +
        + + + + + +
        {{updated}}
        + +
        +
        +
          +
        • + {{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",