From ddc241c0d0372c462f82d170ead677e31761b3f0 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 5 Apr 2016 11:49:27 -0700 Subject: [PATCH 01/38] [Documentation] Add Version Guide Migrate relevant information from existing version guide; add instructions for incrementing versions per #725 --- docs/src/process/cycle.md | 8 +- docs/src/process/index.md | 4 +- docs/src/process/version.md | 142 ++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 docs/src/process/version.md diff --git a/docs/src/process/cycle.md b/docs/src/process/cycle.md index e872044078..4618fe0433 100644 --- a/docs/src/process/cycle.md +++ b/docs/src/process/cycle.md @@ -151,11 +151,9 @@ emphasis on testing. ensuring software passes that testing in order to ship on time; may prefer to disable malfunctioning components and fix them in a subsequent sprint, for example. -* __Ship.__ Tag a code snapshot that has passed acceptance - testing and deploy that version. (Only true if acceptance - testing has passed by this point; if acceptance testing has not +* [__Ship.__](version.md) Tag a code snapshot that has passed release/sprint + testing and deploy that version. (Only true if relevant + testing has passed by this point; if testing has not been passed, will need to make ad hoc decisions with stakeholders, e.g. "extend the sprint" or "defer shipment until end of next sprint.") - - diff --git a/docs/src/process/index.md b/docs/src/process/index.md index 61a22ed6a0..78f919e4c2 100644 --- a/docs/src/process/index.md +++ b/docs/src/process/index.md @@ -3,8 +3,10 @@ The process used to develop Open MCT Web is described in the following documents: -* [Development Cycle](cycle.md): Describes how and when specific +* The [Development Cycle](cycle.md) describes how and when specific process points are repeated during development. +* The [Version Guide](version.md) describes version numbering for + Open MCT (both semantics and process.) * Testing is described in two documents: * The [Test Plan](testing/plan.md) summarizes the approaches used to test Open MCT Web. diff --git a/docs/src/process/version.md b/docs/src/process/version.md new file mode 100644 index 0000000000..aec9fe9248 --- /dev/null +++ b/docs/src/process/version.md @@ -0,0 +1,142 @@ +# Version Guide + +This document describes semantics and processes for providing version +numbers for Open MCT, and additionally provides guidelines for dependent +projects developed by the same team. + +Versions are incremented at specific points in Open MCT's +[Development Cycle](cycle.md); see that document for a description of +sprints and releases. + +## Audience + +Individuals interested in consuming version numbers can be categorized as +follows: + +* _Users_: Generally disinterested, occasionally wish to identify version + to cross-reference against documentation, or to report issues. +* _Testers_: Want to identify which version of the software they are + testing, e.g. to file issues for defects. +* _Internal developers_: Often, inverse of testers; want to identify which + version of software was/is in use when certain behavior is observed. Want + to be able to correlate versions in use with “streams” of development + (e.g. dev vs. prod), when possible. +* _External developers_: Need to understand which version of software is + in use when developing/maintaining plug-ins, in order to ensure + compatibility of their software. + +## Version Reporting + +Software versions should be reflected in the user interface of the +application in three ways: + +* _Version number_: A semantic version (see below) which serves both to + uniquely identify releases, as well as to inform plug-in developers + about compatibility with previous releases. +* _Revision identifier_: While using git, the commit hash. Supports + internal developers and testers by uniquely identifying client + software snapshots. +* _Branding_: Identifies which variant is in use. (Typically, Open MCT + is re-branded when deployed for a specific mission or center.) + +## Version Numbering + +Open MCT shall provide version numbers consistent with +[Semantic Versioning 2.0.0](http://semver.org/). In summary, versions +are expressed in a "major.minor.patch" form, and incremented based on +nature of changes to external API. Breaking changes require a "major" +version increment; backwards-compatible changes require a "minor" +version increment; neutral changes (such as bug fixes) require a "patch" +version increment. A hyphen-separated suffix indicates a pre-release +version, which may be unstable or may not fully meet compatibility +requirements. + +Additionally, the following project-specific standards will be used: + +* During development, a "-SNAPSHOT" suffix shall be appended to the + version number. The version number before the suffix shall reflect + the next expected version number for release. +* Prior to a 1.0.0 release, the _minor_ version will be incremented + on a per-release basis; the _patch_ version will be incremented on a + per-sprint basis. +* Starting at version 1.0.0, version numbers will be updated with each + completed sprint. The version number for the sprint shall be + determined relative to the previous released version; the decision + to increment the _major_, _minor_, or _patch_ version should be + made based on the nature of changes during that release. (It is + recommended that these numbers are incremented as changes are + introduced, such that at end of release the version number may + be chosen by simply removing the suffix.) +* The first three sprints in a release may be unstable; in these cases, a + unique version identifier should still be generated, but a suffix + should be included to indicate that the version is not necessarily + production-ready. Recommended suffixes are: + + Sprint | Suffix +:------:|:--------: + 1 | `-alpha` + 2 | `-beta` + 3 | `-rc` + +### Scope of External API + +"External API" refers to the API exposed to, documented for, and used by +plug-in developers. Changes to interfaces used internally by Open MCT +(or otherwise not documented for use externally) require only a _patch_ +version bump. + +## Incrementing Versions + +At the end of a sprint, the [project manager](cycle.md#roles) +should update (or delegate the task of updating) Open MCT version +numbers by the following process: + +1. Update version number in `package.json` + 1. Remove `-SNAPSHOT` suffix. + 2. Verify that resulting version number meets semantic versioning + requirements relative to previous stable version. Increment if + necessary. + 3. If version is considered unstable (which may be the case during + the first three sprints of a release), apply a new suffix per + [Version Numbering](#version-numbering) guidance above. +2. Tag the release. + 1. Commit changes to `package.json` on the `master` branch. + The commit message should reference the sprint being closed, + preferably by a URL reference to the associated Milestone in + GitHub. + 2. Verify that build still completes, that application passes + smoke-testing, and that only differences from tested versions + are the changes to version number above. + 3. Push the `master` branch. + 4. Tag this commit with the version number, prepending the letter "v". + (e.g. `git tag v0.9.3-alpha`) + 5. Push the tag to GitHub. (e.g. `git push origin v0.9.3-alpha`). +3. Upload a release archive. + 1. Run `npm pack` to generate the archive. + 2. Use the [GitHub release interface](https://github.com/nasa/openmct/releases) + to draft a new release. + 3. Choose the existing tag for the new version (created and pushed above.) + Enter the tag name as the release name as well; see existing releases + for examples. + 4. Attach the release archive. + 5. Designate the release as a "pre-release" as appropriate (for instance, + when the version number has been suffixed as unstable, or when + the version number is below 1.0.0.) +4. Restore snapshot status in `package.json` + 1. Remove any suffix from the version number, or increment the + _patch_ version if there is no suffix. + 2. Append a `-SNAPSHOT` suffix. + 3. Commit changes to `package.json` on the `master` branch. + The commit message should reference the sprint being opened, + preferably by a URL reference to the associated Milestone in + GitHub. + 4. Verify that build still completes, that application passes + smoke-testing. + 5. Push the `master` branch. + +Projects dependent on Open MCT being co-developed by the Open MCT +team should follow a similar process, except that they should +additionally update their dependency on Open MCT to point to the +latest archive when removing their `-SNAPSHOT` status, and +that they should be pointed back to the `master` branch after +this has completed. From 23a8c305c146d35a30c878028d4cfe6d2c24057f Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 3 Apr 2016 14:25:09 -0700 Subject: [PATCH 02/38] [Table] #798 Simplified markup, moved styles to external stylesheet --- platform/features/table/bundle.js | 6 + platform/features/table/res/sass/table.scss | 50 +++++++++ .../table/res/templates/mct-table.html | 43 ++++--- .../src/controllers/MCTTableController.js | 105 +++++++++--------- .../controllers/MCTTableControllerSpec.js | 24 +++- 5 files changed, 148 insertions(+), 80 deletions(-) create mode 100644 platform/features/table/res/sass/table.scss diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js index 77ead67c04..15517c115c 100644 --- a/platform/features/table/bundle.js +++ b/platform/features/table/bundle.js @@ -161,6 +161,12 @@ define([ "key": "table-options-edit", "templateUrl": "templates/table-options-edit.html" } + ], + "stylesheets": [ + { + "stylesheetUrl": "css/table.css", + "priority": "mandatory" + } ] } }); diff --git a/platform/features/table/res/sass/table.scss b/platform/features/table/res/sass/table.scss new file mode 100644 index 0000000000..a79cfac4c6 --- /dev/null +++ b/platform/features/table/res/sass/table.scss @@ -0,0 +1,50 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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. + *****************************************************************************/ +.sizing-table { + min-width: 100%; + z-index: -1; + visibility: hidden; + position: absolute; + + //Add some padding to allow for decorations such as limits indicator + td { + padding-right: 15px; + padding-left: 10px; + white-space: nowrap; + } +} +.mct-table { + table-layout: fixed; + th { + box-sizing: border-box; + } + tbody { + tr { + position: absolute; + } + td { + white-space: nowrap; + overflow: hidden; + box-sizing: border-box; + } + } +} \ No newline at end of file diff --git a/platform/features/table/res/templates/mct-table.html b/platform/features/table/res/templates/mct-table.html index 5997376587..7a18388455 100644 --- a/platform/features/table/res/templates/mct-table.html +++ b/platform/features/table/res/templates/mct-table.html @@ -1,22 +1,25 @@ -
- +
+ + + + + + +
{{header}}
+ {{sizingRow[header].text}} +
+ + }"> - +
@@ -41,21 +42,15 @@
{{ visibleRow.contents[header].text }} diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index ad13647766..c853f155ad 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -23,10 +23,13 @@ define( this.maxDisplayRows = 50; this.scrollable = element.find('div'); + this.thead = element.find('thead'); + this.tbody = element.find('tbody'); + this.$scope.sizingRow = {}; + this.scrollable.on('scroll', this.onScroll.bind(this)); $scope.visibleRows = []; - $scope.overrideRowPositioning = false; /** * Set default values for optional parameters on a given scope @@ -100,14 +103,31 @@ define( * @private */ MCTTableController.prototype.newRow = function (event, rowIndex) { - var row = this.$scope.rows[rowIndex]; - //Add row to the filtered, sorted list of all rows - if (this.filterRows([row]).length > 0) { - this.insertSorted(this.$scope.displayRows, row); + var self = this, + row = this.$scope.rows[rowIndex], + largestRow; + + function sizeAndScroll () { + self.setElementSizes(); + self.scrollToBottom(); } - this.$timeout(this.setElementSizes.bind(this)) - .then(this.scrollToBottom.bind(this)); + //Does the row pass the current filter? + if (this.filterRows([row]).length === 1) { + this.insertSorted(this.$scope.displayRows, row); + + //Calculate largest row + largestRow = this.buildLargestRow([this.$scope.sizingRow, row]); + + // Has it changed? If so, set the the 'sizing' row which + // determines column widths + if (JSON.stringify(largestRow) !== JSON.stringify(this.$scope.sizingRow)){ + this.$scope.sizingRow = largestRow; + this.$timeout(sizeAndScroll); + } else { + sizeAndScroll(); + } + } }; /** @@ -249,13 +269,12 @@ define( * for individual rows. */ MCTTableController.prototype.setElementSizes = function () { - var self = this, - thead = this.element.find('thead'), - tbody = this.element.find('tbody'), + var thead = this.thead, + tbody = this.tbody, firstRow = tbody.find('tr'), column = firstRow.find('td'), headerHeight = thead.prop('offsetHeight'), - rowHeight = 20, + rowHeight = firstRow.prop('offsetHeight'), columnWidth, tableWidth = 0, overallHeight = headerHeight + (rowHeight * @@ -279,8 +298,6 @@ define( } else { this.$scope.totalWidth = 'none'; } - - this.$scope.overrideRowPositioning = true; }; /** @@ -400,43 +417,32 @@ define( * pre-calculate optimal column sizes without having to render * every row. */ - MCTTableController.prototype.findLargestRow = function (rows) { - var largestRow = rows.reduce(function (largestRow, row) { + MCTTableController.prototype.buildLargestRow = function (rows) { + var largestRow = rows.reduce(function (prevLargest, row) { Object.keys(row).forEach(function (key) { - var currentColumn = row[key].text, + var currentColumn, + currentColumnLength, + largestColumn, + largestColumnLength; + if (!row[key]){ + //do nothing, no value for this column; + } else { + currentColumn = (row[key]).text; currentColumnLength = (currentColumn && currentColumn.length) ? currentColumn.length : - currentColumn, - largestColumn = largestRow[key].text, - largestColumnLength = - (largestColumn && largestColumn.length) ? - largestColumn.length : - largestColumn; + currentColumn; + largestColumn = prevLargest[key] ? prevLargest[key].text : ""; + largestColumnLength = largestColumn.length; - if (currentColumnLength > largestColumnLength) { - largestRow[key] = JSON.parse(JSON.stringify(row[key])); + if (currentColumnLength > largestColumnLength) { + prevLargest[key] = JSON.parse(JSON.stringify(row[key])); + } } + }); - return largestRow; + return prevLargest; }, JSON.parse(JSON.stringify(rows[0] || {}))); - - largestRow = JSON.parse(JSON.stringify(largestRow)); - - // Pad with characters to accomodate variable-width fonts, - // and remove characters that would allow word-wrapping. - Object.keys(largestRow).forEach(function (key) { - var padCharacters, - i; - - largestRow[key].text = String(largestRow[key].text); - padCharacters = largestRow[key].text.length / 10; - for (i = 0; i < padCharacters; i++) { - largestRow[key].text = largestRow[key].text + 'W'; - } - largestRow[key].text = largestRow[key].text - .replace(/[ \-_]/g, 'W'); - }); return largestRow; }; @@ -447,20 +453,13 @@ define( * @private */ MCTTableController.prototype.resize = function (){ - var largestRow = this.findLargestRow(this.$scope.displayRows), - self = this; - this.$scope.visibleRows = [ - { - rowIndex: 0, - offsetY: undefined, - contents: largestRow - } - ]; + var self = this; + + this.$scope.sizingRow = this.buildLargestRow(this.$scope.displayRows); //Wait a timeout to allow digest of previous change to visible // rows to happen. this.$timeout(function () { - //Remove temporary padding row used for setting column widths self.$scope.visibleRows = []; self.setElementSizes(); }); @@ -489,8 +488,6 @@ define( //Reset visible rows because new row data available. this.$scope.visibleRows = []; - this.$scope.overrideRowPositioning = false; - //Nothing to show because no columns visible if (!this.$scope.displayHeaders) { return; diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 5e38c7e651..5ee9f21c41 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -58,15 +58,18 @@ define( mockElement = jasmine.createSpyObj('element', [ 'find', + 'prop', 'on' ]); mockElement.find.andReturn(mockElement); + mockElement.prop.andReturn(0); mockScope.displayHeaders = true; mockTimeout = jasmine.createSpy('$timeout'); mockTimeout.andReturn(promise(undefined)); controller = new MCTTableController(mockScope, mockTimeout, mockElement); + spyOn(controller, 'setVisibleRows'); }); it('Reacts to changes to filters, headers, and rows', function() { @@ -138,8 +141,6 @@ define( var removeRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-1].args[1]; controller.updateRows(testRows); expect(mockScope.displayRows.length).toBe(3); - spyOn(controller, 'setVisibleRows'); - //controller.setVisibleRows.andReturn(undefined); removeRowFunc(undefined, 2); expect(mockScope.displayRows.length).toBe(2); expect(controller.setVisibleRows).toHaveBeenCalled(); @@ -266,6 +267,25 @@ define( expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); }); + it('Resizes columns if length of any columns in new' + + ' row exceeds corresponding existing column', function() { + var row7 = { + 'col1': {'text': 'row6 col1'}, + 'col2': {'text': 'some longer string'}, + 'col3': {'text': 'row6 col3'} + }; + + mockScope.sortColumn = undefined; + mockScope.sortDirection = undefined; + mockScope.filters = {}; + + mockScope.displayRows = testRows.slice(0); + + mockScope.rows.push(row7); + controller.newRow(undefined, mockScope.rows.length-1); + expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); + }); + }); }); From 99ba9edb95ffb8778a73bfbf2a4bfb71feed4ea6 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 11 Apr 2016 12:42:15 -0700 Subject: [PATCH 03/38] Merged Resolved merge conflicts Resolved merge conflicts --- platform/features/table/bundle.js | 18 ++-- .../{table.html => historical-table.html} | 2 +- .../table/res/templates/rt-table.html | 2 +- .../features/table/src/TableConfiguration.js | 25 ++++- .../controllers/HistoricalTableController.js | 70 ++++++++++++ .../src/controllers/MCTTableController.js | 101 +++++++++--------- ...ntroller.js => RealtimeTableController.js} | 71 ++++-------- .../src/controllers/TableOptionsController.js | 24 ++++- .../controllers/TelemetryTableController.js | 94 +++++++--------- .../table/test/TableConfigurationSpec.js | 11 +- ...ec.js => HistoricalTableControllerSpec.js} | 17 ++- .../controllers/MCTTableControllerSpec.js | 24 ++--- ...Spec.js => RealtimeTableControllerSpec.js} | 13 ++- .../controllers/TableOptionsControllerSpec.js | 10 +- 14 files changed, 273 insertions(+), 209 deletions(-) rename platform/features/table/res/templates/{table.html => historical-table.html} (71%) create mode 100644 platform/features/table/src/controllers/HistoricalTableController.js rename platform/features/table/src/controllers/{RTTelemetryTableController.js => RealtimeTableController.js} (58%) rename platform/features/table/test/controllers/{TelemetryTableControllerSpec.js => HistoricalTableControllerSpec.js} (94%) rename platform/features/table/test/controllers/{RTTelemetryTableControllerSpec.js => RealtimeTableControllerSpec.js} (94%) diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js index 15517c115c..0220b7dc6d 100644 --- a/platform/features/table/bundle.js +++ b/platform/features/table/bundle.js @@ -23,16 +23,16 @@ define([ "./src/directives/MCTTable", - "./src/controllers/RTTelemetryTableController", - "./src/controllers/TelemetryTableController", + "./src/controllers/RealtimeTableController", + "./src/controllers/HistoricalTableController", "./src/controllers/TableOptionsController", '../../commonUI/regions/src/Region', '../../commonUI/browse/src/InspectorRegion', "legacyRegistry" ], function ( MCTTable, - RTTelemetryTableController, - TelemetryTableController, + RealtimeTableController, + HistoricalTableController, TableOptionsController, Region, InspectorRegion, @@ -109,13 +109,13 @@ define([ ], "controllers": [ { - "key": "TelemetryTableController", - "implementation": TelemetryTableController, + "key": "HistoricalTableController", + "implementation": HistoricalTableController, "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] }, { - "key": "RTTelemetryTableController", - "implementation": RTTelemetryTableController, + "key": "RealtimeTableController", + "implementation": RealtimeTableController, "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] }, { @@ -130,7 +130,7 @@ define([ "name": "Historical Table", "key": "table", "glyph": "\ue604", - "templateUrl": "templates/table.html", + "templateUrl": "templates/historical-table.html", "needs": [ "telemetry" ], diff --git a/platform/features/table/res/templates/table.html b/platform/features/table/res/templates/historical-table.html similarity index 71% rename from platform/features/table/res/templates/table.html rename to platform/features/table/res/templates/historical-table.html index c63f6d63fc..9917c41dcc 100644 --- a/platform/features/table/res/templates/table.html +++ b/platform/features/table/res/templates/historical-table.html @@ -1,4 +1,4 @@ -
+
+
0) { this.$scope.totalWidth = tableWidth + 'px'; @@ -439,7 +434,6 @@ define( prevLargest[key] = JSON.parse(JSON.stringify(row[key])); } } - }); return prevLargest; }, JSON.parse(JSON.stringify(rows[0] || {}))); @@ -447,26 +441,30 @@ define( }; /** - * Calculates the widest row in the table, pads that row, and adds - * it to the table. Allows the table to size itself, then uses this - * as basis for column dimensions. + * Calculates the widest row in the table, and if necessary, resizes + * the table accordingly + * + * @param rows the rows on which to resize + * @returns {Promise} a promise that will resolve when resizing has + * occurred. * @private */ - MCTTableController.prototype.resize = function (){ - var self = this; + MCTTableController.prototype.resize = function (rows){ + //Calculate largest row + var largestRow = this.buildLargestRow(rows); - this.$scope.sizingRow = this.buildLargestRow(this.$scope.displayRows); - - //Wait a timeout to allow digest of previous change to visible - // rows to happen. - this.$timeout(function () { - self.$scope.visibleRows = []; - self.setElementSizes(); - }); + // Has it changed? If so, set the the 'sizing' row which + // determines column widths + if (JSON.stringify(largestRow) !== JSON.stringify(this.$scope.sizingRow)){ + this.$scope.sizingRow = largestRow; + return this.$timeout(this.setElementSizes.bind(this)); + } else { + return fastPromise(undefined); + } }; /** - * @priate + * @private */ MCTTableController.prototype.filterAndSort = function (rows) { var displayRows = rows; @@ -484,17 +482,14 @@ define( * Update rows with new data. If filtering is enabled, rows * will be sorted before display. */ - MCTTableController.prototype.updateRows = function (newRows) { - //Reset visible rows because new row data available. - this.$scope.visibleRows = []; - + MCTTableController.prototype.setRows = function (newRows) { //Nothing to show because no columns visible - if (!this.$scope.displayHeaders) { + if (!this.$scope.displayHeaders || !newRows) { return; } this.filterAndSort(newRows || []); - this.resize(); + this.resize(newRows).then(this.setVisibleRows.bind(this)); }; /** diff --git a/platform/features/table/src/controllers/RTTelemetryTableController.js b/platform/features/table/src/controllers/RealtimeTableController.js similarity index 58% rename from platform/features/table/src/controllers/RTTelemetryTableController.js rename to platform/features/table/src/controllers/RealtimeTableController.js index 8a61d61b5e..cf58cb236e 100644 --- a/platform/features/table/src/controllers/RTTelemetryTableController.js +++ b/platform/features/table/src/controllers/RealtimeTableController.js @@ -37,7 +37,7 @@ define( * @param telemetryFormatter * @constructor */ - function RTTelemetryTableController($scope, telemetryHandler, telemetryFormatter) { + function RealtimeTableController($scope, telemetryHandler, telemetryFormatter) { TableController.call(this, $scope, telemetryHandler, telemetryFormatter); $scope.autoScroll = false; @@ -66,58 +66,31 @@ define( }); } - RTTelemetryTableController.prototype = Object.create(TableController.prototype); + RealtimeTableController.prototype = Object.create(TableController.prototype); - /** - Override the subscribe function defined on the parent controller in - order to handle realtime telemetry instead of historical. - */ - RTTelemetryTableController.prototype.subscribe = function () { - var self = this; - self.$scope.rows = undefined; - (this.subscriptions || []).forEach(function (unsubscribe){ - unsubscribe(); - }); + RealtimeTableController.prototype.addRealtimeData = function() { + var self = this, + datum, + row; + this.handle.getTelemetryObjects().forEach(function (telemetryObject){ + datum = self.handle.getDatum(telemetryObject); + if (datum) { + //Populate row values from telemetry datum + row = self.table.getRowValues(telemetryObject, datum); + self.$scope.rows.push(row); - if (this.handle) { - this.handle.unsubscribe(); - } - - function updateData(){ - var datum, - row; - self.handle.getTelemetryObjects().forEach(function (telemetryObject){ - datum = self.handle.getDatum(telemetryObject); - if (datum) { - row = self.table.getRowValues(telemetryObject, datum); - if (!self.$scope.rows){ - self.$scope.rows = [row]; - self.$scope.$digest(); - } else { - self.$scope.rows.push(row); - - if (self.$scope.rows.length > self.maxRows) { - self.$scope.$broadcast('remove:row', 0); - self.$scope.rows.shift(); - } - - self.$scope.$broadcast('add:row', - self.$scope.rows.length - 1); - } + //Inform table that a new row has been added + if (self.$scope.rows.length > self.maxRows) { + self.$scope.$broadcast('remove:row', 0); + self.$scope.rows.shift(); } - }); - } + self.$scope.$broadcast('add:row', + self.$scope.rows.length - 1); + } + }); + } - this.handle = this.$scope.domainObject && this.telemetryHandler.handle( - this.$scope.domainObject, - updateData, - true // Lossless - ); - - this.setup(); - }; - - return RTTelemetryTableController; + return RealtimeTableController; } ); diff --git a/platform/features/table/src/controllers/TableOptionsController.js b/platform/features/table/src/controllers/TableOptionsController.js index c3b479073c..499e91efbc 100644 --- a/platform/features/table/src/controllers/TableOptionsController.js +++ b/platform/features/table/src/controllers/TableOptionsController.js @@ -51,13 +51,22 @@ define( this.$scope = $scope; this.domainObject = $scope.domainObject; + this.listeners = []; $scope.columnsForm = {}; - this.domainObject.getCapability('mutation').listen(function (model) { - self.populateForm(model); + $scope.$watch('domainObject', function(domainObject) { + self.populateForm(domainObject.getModel()); + + self.listeners.push(self.domainObject.getCapability('mutation').listen(function (model) { + self.populateForm(model); + })); }); + /** + * Maintain a configuration object on scope that stores column + * configuration. On change, synchronize with object model. + */ $scope.$watchCollection('configuration.table.columns', function (columns){ if (columns){ self.domainObject.useCapability('mutation', function (model) { @@ -67,6 +76,15 @@ define( } }); + /** + * Destroy all mutation listeners + */ + $scope.$on('$destroy', function () { + self.listeners.forEach(function (listener) { + listener(); + }); + }) + } TableOptionsController.prototype.populateForm = function (model) { @@ -86,7 +104,7 @@ define( 'key': key }); }); - this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration)); + this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration || {})); }; return TableOptionsController; diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index e579c5eeb8..b41cb940f5 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -52,19 +52,15 @@ define( this.$scope = $scope; this.columns = {}; //Range and Domain columns this.handle = undefined; - //this.pending = false; this.telemetryHandler = telemetryHandler; this.table = new TableConfiguration($scope.domainObject, telemetryFormatter); this.changeListeners = []; - $scope.rows = undefined; + $scope.rows = []; // Subscribe to telemetry when a domain object becomes available - this.$scope.$watch('domainObject', function(domainObject){ - if (!domainObject) - return; - + this.$scope.$watch('domainObject', function(){ self.subscribe(); self.registerChangeListeners(); }); @@ -73,16 +69,24 @@ define( this.$scope.$on("$destroy", this.destroy.bind(this)); } + /** + * @private + */ + TelemetryTableController.prototype.unregisterChangeListeners = function () { + this.changeListeners.forEach(function (listener) { + return listener && listener(); + }); + this.changeListeners = []; + } + /** * Defer registration of change listeners until domain object is * available in order to avoid race conditions * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { - this.changeListeners.forEach(function (listener) { - return listener && listener(); - }); - this.changeListeners = []; + this.unregisterChangeListeners(); + // When composition changes, re-subscribe to the various // telemetry subscriptions this.changeListeners.push(this.$scope.$watchCollection( @@ -103,25 +107,37 @@ define( } }; + /** + * Function for handling realtime data when it is available. This + * will be called by the telemetry framework when new data is + * available. + * + * Method should be overridden by specializing class. + */ + TelemetryTableController.prototype.addRealtimeData = function () { + }; + + /** + * Function for handling historical data. Will be called by + * telemetry framework when requested historical data is available. + * Should be overridden by specializing class. + */ + TelemetryTableController.prototype.addHistoricalData = function () { + }; + /** Create a new subscription. This can be overridden by children to change default behaviour (which is to retrieve historical telemetry only). */ TelemetryTableController.prototype.subscribe = function () { - var self = this; - if (this.handle) { this.handle.unsubscribe(); } - //Noop because not supporting realtime data right now - function noop(){ - } - this.handle = this.$scope.domainObject && this.telemetryHandler.handle( this.$scope.domainObject, - noop, + this.addRealtimeData.bind(this), true // Lossless ); @@ -130,28 +146,6 @@ define( this.setup(); }; - /** - * Populates historical data on scope when it becomes available - * @private - */ - TelemetryTableController.prototype.addHistoricalData = function () { - var rowData = [], - self = this; - - this.handle.getTelemetryObjects().forEach(function (telemetryObject){ - var series = self.handle.getSeries(telemetryObject) || {}, - pointCount = series.getPointCount ? series.getPointCount() : 0, - i = 0; - - for (; i < pointCount; i++) { - rowData.push(self.table.getRowValues(telemetryObject, - self.handle.makeDatum(telemetryObject, series, i))); - } - }); - - this.$scope.rows = rowData; - }; - /** * Setup table columns based on domain object metadata */ @@ -162,7 +156,9 @@ define( if (handle) { handle.promiseTelemetryObjects().then(function () { - table.buildColumns(handle.getMetadata()); + self.$scope.headers = [] + self.$scope.rows = []; + table.populateColumns(handle.getMetadata()); self.filterColumns(); @@ -176,26 +172,14 @@ define( } }; - /** - * @private - * @param object The object for which data is available (table may - * be composed of multiple objects) - * @param datum The data received from the telemetry source - */ - TelemetryTableController.prototype.updateRows = function (object, datum) { - this.$scope.rows.push(this.table.getRowValues(object, datum)); - }; - /** * When column configuration changes, update the visible headers * accordingly. * @private */ - TelemetryTableController.prototype.filterColumns = function (columnConfig) { - if (!columnConfig){ - columnConfig = this.table.getColumnConfiguration(); - this.table.saveColumnConfiguration(columnConfig); - } + TelemetryTableController.prototype.filterColumns = function () { + var columnConfig = this.table.buildColumnConfiguration(); + //Populate headers with visible columns (determined by configuration) this.$scope.headers = Object.keys(columnConfig).filter(function (column) { return columnConfig[column]; diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js index 86a18aee5a..79583d09f8 100644 --- a/platform/features/table/test/TableConfigurationSpec.js +++ b/platform/features/table/test/TableConfigurationSpec.js @@ -116,10 +116,10 @@ define( }]; beforeEach(function() { - table.buildColumns(metadata); + table.populateColumns(metadata); }); - it("populates the columns attribute", function() { + it("populates columns", function() { expect(table.columns.length).toBe(5); }); @@ -141,7 +141,7 @@ define( it("Provides a default configuration with all columns" + " visible", function() { - var configuration = table.getColumnConfiguration(); + var configuration = table.buildColumnConfiguration(); expect(configuration).toBeDefined(); expect(Object.keys(configuration).every(function(key){ @@ -160,7 +160,7 @@ define( }; mockModel.configuration = modelConfig; - tableConfig = table.getColumnConfiguration(); + tableConfig = table.buildColumnConfiguration(); expect(tableConfig).toBeDefined(); expect(tableConfig['Range 1']).toBe(false); @@ -191,6 +191,9 @@ define( expect(mockTelemetryFormatter.formatRangeValue).toHaveBeenCalled(); }); }); + /** + * TODO: Add test for saving column config + */ }); }); } diff --git a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js b/platform/features/table/test/controllers/HistoricalTableControllerSpec.js similarity index 94% rename from platform/features/table/test/controllers/TelemetryTableControllerSpec.js rename to platform/features/table/test/controllers/HistoricalTableControllerSpec.js index 03f62f11e3..f000529467 100644 --- a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js +++ b/platform/features/table/test/controllers/HistoricalTableControllerSpec.js @@ -23,7 +23,7 @@ define( [ - "../../src/controllers/TelemetryTableController" + "../../src/controllers/HistoricalTableController" ], function (TableController) { "use strict"; @@ -73,14 +73,14 @@ define( mockTable = jasmine.createSpyObj('table', [ - 'buildColumns', - 'getColumnConfiguration', + 'populateColumns', + 'buildColumnConfiguration', 'getRowValues', 'saveColumnConfiguration' ] ); mockTable.columns = []; - mockTable.getColumnConfiguration.andReturn(mockConfiguration); + mockTable.buildColumnConfiguration.andReturn(mockConfiguration); mockDomainObject= jasmine.createSpyObj('domainObject', [ 'getCapability', @@ -126,21 +126,18 @@ define( expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled(); }); - describe('the controller makes use of the table', function () { + describe('makes use of the table', function () { it('to create column definitions from telemetry' + ' metadata', function () { controller.setup(); - expect(mockTable.buildColumns).toHaveBeenCalled(); + expect(mockTable.populateColumns).toHaveBeenCalled(); }); it('to create column configuration, which is written to the' + ' object model', function () { - var mockModel = {}; - controller.setup(); - expect(mockTable.getColumnConfiguration).toHaveBeenCalled(); - expect(mockTable.saveColumnConfiguration).toHaveBeenCalled(); + expect(mockTable.buildColumnConfiguration).toHaveBeenCalled(); }); }); diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 5ee9f21c41..7bca5c65b4 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -118,7 +118,7 @@ define( }); it('Sets rows on scope when rows change', function() { - controller.updateRows(testRows); + controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); expect(mockScope.displayRows).toEqual(testRows); }); @@ -130,7 +130,7 @@ define( 'col2': {'text': 'ghi'}, 'col3': {'text': 'row3 col3'} }; - controller.updateRows(testRows); + controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); testRows.push(row4); addRowFunc(undefined, 3); @@ -139,7 +139,7 @@ define( it('Supports removing rows individually', function() { var removeRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-1].args[1]; - controller.updateRows(testRows); + controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); removeRowFunc(undefined, 2); expect(mockScope.displayRows.length).toBe(2); @@ -211,20 +211,20 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); mockScope.rows.push(row4); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); mockScope.rows.push(row5); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[4].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); //Add a duplicate row mockScope.rows.push(row6); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); }); @@ -240,12 +240,12 @@ define( mockScope.displayRows = controller.filterRows(testRows); mockScope.rows.push(row5); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows[1].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows.length).toBe(2); //Row was not added because does not match filter }); @@ -259,11 +259,11 @@ define( mockScope.displayRows = testRows.slice(0); mockScope.rows.push(row5); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[3].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); }); @@ -282,7 +282,7 @@ define( mockScope.displayRows = testRows.slice(0); mockScope.rows.push(row7); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); }); diff --git a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js b/platform/features/table/test/controllers/RealtimeTableControllerSpec.js similarity index 94% rename from platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js rename to platform/features/table/test/controllers/RealtimeTableControllerSpec.js index 59911d1771..e0b6978d88 100644 --- a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js +++ b/platform/features/table/test/controllers/RealtimeTableControllerSpec.js @@ -23,7 +23,7 @@ define( [ - "../../src/controllers/RTTelemetryTableController" + "../../src/controllers/RealtimeTableController" ], function (TableController) { "use strict"; @@ -77,14 +77,14 @@ define( mockTable = jasmine.createSpyObj('table', [ - 'buildColumns', - 'getColumnConfiguration', + 'populateColumns', + 'buildColumnConfiguration', 'getRowValues', 'saveColumnConfiguration' ] ); mockTable.columns = []; - mockTable.getColumnConfiguration.andReturn(mockConfiguration); + mockTable.buildColumnConfiguration.andReturn(mockConfiguration); mockTable.getRowValues.andReturn(mockTableRow); mockDomainObject= jasmine.createSpyObj('domainObject', [ @@ -107,13 +107,16 @@ define( 'unsubscribe', 'getDatum', 'promiseTelemetryObjects', - 'getTelemetryObjects' + 'getTelemetryObjects', + 'request' ]); + // Arbitrary array with non-zero length, contents are not // used by mocks mockTelemetryHandle.getTelemetryObjects.andReturn([{}]); mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); mockTelemetryHandle.getDatum.andReturn({}); + mockTelemetryHandle.request.andReturn(promise(undefined)); mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ 'handle' diff --git a/platform/features/table/test/controllers/TableOptionsControllerSpec.js b/platform/features/table/test/controllers/TableOptionsControllerSpec.js index 9de96b5f52..483696e0e2 100644 --- a/platform/features/table/test/controllers/TableOptionsControllerSpec.js +++ b/platform/features/table/test/controllers/TableOptionsControllerSpec.js @@ -47,11 +47,16 @@ define( 'listen' ]); mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability' + 'getCapability', + 'getModel' ]); mockDomainObject.getCapability.andReturn(mockCapability); + mockDomainObject.getModel.andReturn({}); + mockScope = jasmine.createSpyObj('scope', [ - '$watchCollection' + '$watchCollection', + '$watch', + '$on' ]); mockScope.domainObject = mockDomainObject; @@ -59,6 +64,7 @@ define( }); it('Registers a listener for mutation events on the object', function() { + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); expect(mockCapability.listen).toHaveBeenCalled(); }); From 20672ad028fa382d52ad70496aff85b041e440a6 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 4 Apr 2016 21:10:13 -0700 Subject: [PATCH 04/38] [Tables] #801 Documented MctTable directive --- docs/src/guide/index.md | 68 +++++++++++++++++-- .../src/controllers/MCTTableController.js | 2 +- .../controllers/RealtimeTableController.js | 6 +- .../src/controllers/TableOptionsController.js | 15 ++-- .../controllers/TelemetryTableController.js | 4 +- .../features/table/src/directives/MCTTable.js | 45 ++++++++++++ .../controllers/TableOptionsControllerSpec.js | 12 ++++ 7 files changed, 136 insertions(+), 16 deletions(-) diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index 93cb95bb7a..7016462a5a 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -6,12 +6,13 @@ Victor Woeltjen September 23, 2015 Document Version 1.1 -Date | Version | Summary of Changes | Author -------------------- | --------- | ----------------------- | --------------- -April 29, 2015 | 0 | Initial Draft | Victor Woeltjen -May 12, 2015 | 0.1 | | Victor Woeltjen -June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen -October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry +Date | Version | Summary of Changes | Author +------------------- | --------- | ------------------------- | --------------- +April 29, 2015 | 0 | Initial Draft | Victor Woeltjen +May 12, 2015 | 0.1 | | Victor Woeltjen +June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen +October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry +April 5, 2016 | 1.2 | Added Mct-table directive | Andrew Henry # Introduction The purpose of this guide is to familiarize software developers with the Open @@ -1600,6 +1601,61 @@ there are items . ] } +## Table + +The `mct-table` directive provides a generic table component, with optional +sorting and filtering capabilities. The table can be pre-populated with data +by setting the `rows` parameter, and it can be updated in real-time using the +`add:row` and `remove:row` broadcast events. The table will expand to occupy +100% of the size of its containing element. The table is highly optimized for +very large data sets. + +### Events + +The table supports two events for notifying that the rows have changed. For +performance reasons, the table does not monitor the content of `rows` +constantly. + +* `add:row`: A `$broadcast` event that will notify the table that a new row +has been added to the table. + +eg. The code below adds a new row, and alerts the table using the `add:row` +event. Sorting and filtering will be applied automatically by the table component. + +``` +$scope.rows.push(newRow); +$scope.$broadcast('add:row', $scope.rows.length-1); +``` + +* `remove:row`: A `$broadcast` event that will notify the table that a row +should be removed from the table. + +eg. The code below removes a row from the rows array, and then alerts the table +to its removal. + +``` +$scope.rows.slice(5, 1); +$scope.$broadcast('remove:row', 5); +``` + +### Parameters + +* `headers`: An array of string values which will constitute the column titles + that appear at the top of the table. Corresponding values are specified in + the rows using the header title provided here. +* `rows`: An array of objects containing row values. Each element in the +array must be an associative array, where the key corresponds to a column header. +* `enableFilter`: A boolean that if true, will enable searching and result +filtering. When enabled, each column will have a text input field that can be +used to filter the table rows in real time. +* `enableSort`: A boolean determining whether rows can be sorted. If true, +sorting will be enabled allowing sorting by clicking on column headers. Only +one column may be sorted at a time. +* `autoScroll`: A boolean value that if true, will cause the table to automatically +scroll to the bottom as new data arrives. Auto-scroll can be disengaged manually +by scrolling away from the bottom of the table, and can also be enabled manually +by scrolling to the bottom of the table rows. + # Services The Open MCT Web platform provides a variety of services which can be retrieved diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index 477707e84b..7b4c748c7b 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -85,7 +85,7 @@ define( then: function (callback) { return fastPromise(callback(returnValue)); } - } + }; } /** diff --git a/platform/features/table/src/controllers/RealtimeTableController.js b/platform/features/table/src/controllers/RealtimeTableController.js index cf58cb236e..3f983207bc 100644 --- a/platform/features/table/src/controllers/RealtimeTableController.js +++ b/platform/features/table/src/controllers/RealtimeTableController.js @@ -68,6 +68,10 @@ define( RealtimeTableController.prototype = Object.create(TableController.prototype); + /** + * Overrides method on TelemetryTableController providing handling + * for realtime data. + */ RealtimeTableController.prototype.addRealtimeData = function() { var self = this, datum, @@ -89,7 +93,7 @@ define( self.$scope.rows.length - 1); } }); - } + }; return RealtimeTableController; } diff --git a/platform/features/table/src/controllers/TableOptionsController.js b/platform/features/table/src/controllers/TableOptionsController.js index 499e91efbc..a28411e7d0 100644 --- a/platform/features/table/src/controllers/TableOptionsController.js +++ b/platform/features/table/src/controllers/TableOptionsController.js @@ -55,8 +55,15 @@ define( $scope.columnsForm = {}; + function unlisten() { + self.listeners.forEach(function (listener) { + listener(); + }); + } + $scope.$watch('domainObject', function(domainObject) { - self.populateForm(domainObject.getModel()); + unlisten(); + self.populateForm(domainObject.getModel()); self.listeners.push(self.domainObject.getCapability('mutation').listen(function (model) { self.populateForm(model); @@ -79,11 +86,7 @@ define( /** * Destroy all mutation listeners */ - $scope.$on('$destroy', function () { - self.listeners.forEach(function (listener) { - listener(); - }); - }) + $scope.$on('$destroy', unlisten); } diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index b41cb940f5..36f54b1ac6 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -77,7 +77,7 @@ define( return listener && listener(); }); this.changeListeners = []; - } + }; /** * Defer registration of change listeners until domain object is @@ -156,7 +156,7 @@ define( if (handle) { handle.promiseTelemetryObjects().then(function () { - self.$scope.headers = [] + self.$scope.headers = []; self.$scope.rows = []; table.populateColumns(handle.getMetadata()); diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js index 575e830395..2d61669a2e 100644 --- a/platform/features/table/src/directives/MCTTable.js +++ b/platform/features/table/src/directives/MCTTable.js @@ -12,6 +12,51 @@ define( * Defines a generic 'Table' component. The table can be populated * en-masse by setting the rows attribute, or rows can be added as * needed via a broadcast 'addRow' event. + * + * This directive accepts parameters specifying header and row + * content, as well as some additional options. + * + * Two broadcast events for notifying the table that the rows have + * changed. For performance reasons, the table does not monitor the + * content of `rows` constantly. + * - 'add:row': A $broadcast event that will notify the table that + * a new row has been added to the table. + * eg. + *

+         * $scope.rows.push(newRow);
+         * $scope.$broadcast('add:row', $scope.rows.length-1);
+         * 
+ * The code above adds a new row, and alerts the table using the + * add:row event. Sorting and filtering will be applied + * automatically by the table component. + * + * - 'remove:row': A $broadcast event that will notify the table that a + * row should be removed from the table. + * eg. + *

+         * $scope.rows.slice(5, 1);
+         * $scope.$broadcast('remove:row', 5);
+         * 
+ * The code above removes a row from the rows array, and then alerts + * the table to its removal. + * + * @memberof platform/features/table + * @param {string[]} headers The column titles to appear at the top + * of the table. Corresponding values are specified in the rows + * using the header title provided here. + * @param {Object[]} rows The row content. Each row is an object + * with key-value pairs where the key corresponds to a header + * specified in the headers parameter. + * @param {boolean} enableFilter If true, values will be searchable + * and results filtered + * @param {boolean} enableSort If true, sorting will be enabled + * allowing sorting by clicking on column headers + * @param {boolean} autoScroll If true, table will automatically + * scroll to the bottom as new data arrives. Auto-scroll can be + * disengaged manually by scrolling away from the bottom of the + * table, and can also be enabled manually by scrolling to the bottom of + * the table rows. + * * @constructor */ function MCTTable($timeout) { diff --git a/platform/features/table/test/controllers/TableOptionsControllerSpec.js b/platform/features/table/test/controllers/TableOptionsControllerSpec.js index 483696e0e2..fd6d8b43fe 100644 --- a/platform/features/table/test/controllers/TableOptionsControllerSpec.js +++ b/platform/features/table/test/controllers/TableOptionsControllerSpec.js @@ -63,6 +63,18 @@ define( controller = new TableOptionsController(mockScope); }); + it('Listens for changing domain object', function() { + expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function)); + }); + + it('On destruction of controller, destroys listeners', function() { + var unlistenFunc = jasmine.createSpy("unlisten"); + controller.listeners.push(unlistenFunc); + expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function)); + mockScope.$on.mostRecentCall.args[1](); + expect(unlistenFunc).toHaveBeenCalled(); + }); + it('Registers a listener for mutation events on the object', function() { mockScope.$watch.mostRecentCall.args[1](mockDomainObject); expect(mockCapability.listen).toHaveBeenCalled(); From 0c6b4a5a231a71100ce5d3a3b0d18fdb66f77578 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 5 Apr 2016 12:05:11 -0700 Subject: [PATCH 05/38] [Tables] Fix to correct sorting in realtime tables --- .../src/controllers/MCTTableController.js | 29 ++++++++---- .../controllers/MCTTableControllerSpec.js | 46 ++++++++++++++++++- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index 7b4c748c7b..2fe7fc1ba6 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -295,6 +295,20 @@ define( } }; + /** + * Given a value, if it can be coerced to a number, then return a + * number representation. It's a little more robust than using just + * Number() or parseFloat, or isNaN in isolation, all of which are + * fairly inconsistent in their results. + * @param value The value to cast (if possible) + * @returns {*} The value cast to a Number, or the original value if + * a Number representation is not possible. + */ + MCTTableController.prototype.toNumber = function (value){ + var val = !isNaN(Number(value)) && !isNaN(parseFloat(value)) ? Number(value) : value; + return val; + }; + /** * @private */ @@ -311,12 +325,8 @@ define( return min; // Element is not in array, min gives direction } - valA = isNaN(searchElement[sortKey].text) ? - searchElement[sortKey].text : - parseFloat(searchElement[sortKey].text); - valB = isNaN(searchArray[sampleAt][sortKey].text) ? - searchArray[sampleAt][sortKey].text : - parseFloat(searchArray[sampleAt][sortKey].text); + valA = self.toNumber(searchElement[sortKey].text); + valB = self.toNumber(searchArray[sampleAt][sortKey].text); switch(self.sortComparator(valA, valB)) { case -1: @@ -397,10 +407,9 @@ define( //If the values to compare can be compared as // numbers, do so. String comparison of number // values can cause inconsistencies - var valA = isNaN(a[sortKey].text) ? a[sortKey].text : - parseFloat(a[sortKey].text), - valB = isNaN(b[sortKey].text) ? b[sortKey].text : - parseFloat(b[sortKey].text); + + var valA = self.toNumber(a[sortKey].text), + valB = self.toNumber(b[sortKey].text); return self.sortComparator(valA, valB); }); diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 7bca5c65b4..436b53bb16 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -180,6 +180,50 @@ define( expect(sortedRows[2].col2.text).toEqual('abc'); }); + it('converts number strings to numbers', function () { + var val1 = "", + val2 = "1", + val3 = "2016-04-05 18:41:30.713Z", + val4 = "1.1", + val5 = "8.945520958175627e-13"; + + expect(controller.toNumber(val1)).toEqual(""); + expect(controller.toNumber(val2)).toEqual(1); + expect(controller.toNumber(val3)).toEqual("2016-04-05 18:41:30.713Z"); + expect(controller.toNumber(val4)).toEqual(1.1); + expect(controller.toNumber(val5)).toEqual(8.945520958175627e-13); + }); + + it('correctly sorts rows of differing types', function () { + mockScope.sortColumn = 'col2'; + mockScope.sortDirection = 'desc'; + + testRows.push({ + 'col1': {'text': 'row4 col1'}, + 'col2': {'text': '123'}, + 'col3': {'text': 'row4 col3'} + }); + testRows.push({ + 'col1': {'text': 'row5 col1'}, + 'col2': {'text': '456'}, + 'col3': {'text': 'row5 col3'} + }); + testRows.push({ + 'col1': {'text': 'row5 col1'}, + 'col2': {'text': ''}, + 'col3': {'text': 'row5 col3'} + }); + + sortedRows = controller.sortRows(testRows); + expect(sortedRows[0].col2.text).toEqual('ghi'); + expect(sortedRows[1].col2.text).toEqual('def'); + expect(sortedRows[2].col2.text).toEqual('abc'); + + expect(sortedRows[sortedRows.length-3].col2.text).toEqual('456'); + expect(sortedRows[sortedRows.length-2].col2.text).toEqual('123'); + expect(sortedRows[sortedRows.length-1].col2.text).toEqual(''); + }); + describe('Adding new rows', function() { var row4, row5, @@ -251,7 +295,7 @@ define( }); it('Adds new rows at the correct sort position when' + - ' not sorted ', function() { + ' not sorted ', function () { mockScope.sortColumn = undefined; mockScope.sortDirection = undefined; mockScope.filters = {}; From 2fb9b65652d80d5a9640d0bed3cbbde897957539 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 7 Apr 2016 11:14:02 -0700 Subject: [PATCH 06/38] Made comment a little more accurate --- .../features/table/src/controllers/MCTTableController.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index 2fe7fc1ba6..a90d660f3c 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -296,11 +296,12 @@ define( }; /** - * Given a value, if it can be coerced to a number, then return a - * number representation. It's a little more robust than using just + * Given a value, if it is a number, or a string representation of a + * number, then return a number representation. Otherwise, return + * the original value. It's a little more robust than using just * Number() or parseFloat, or isNaN in isolation, all of which are * fairly inconsistent in their results. - * @param value The value to cast (if possible) + * @param value The value to return as a number. * @returns {*} The value cast to a Number, or the original value if * a Number representation is not possible. */ From f34e8ba61b1acdcbe21edbf8bef5dba523ac91db Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 11 Apr 2016 13:09:02 -0700 Subject: [PATCH 07/38] Modified code to call resize on every row add. Removed optimization to only resize when needed, because in fact resuze is necessary on every update in order to set vertical scroll size --- .../src/controllers/MCTTableController.js | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index a90d660f3c..9b65613516 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -80,14 +80,6 @@ define( $scope.$on('remove:row', this.removeRow.bind(this)); } - function fastPromise(returnValue) { - return { - then: function (callback) { - return fastPromise(callback(returnValue)); - } - }; - } - /** * If auto-scroll is enabled, this function will scroll to the * bottom of the page @@ -118,8 +110,7 @@ define( //Insert the row into the correct position in the array this.insertSorted(this.$scope.displayRows, row); - //Resize the columns , then update the rows - // visible in the table + //Resize the columns , then update the rows visible in the table this.resize([this.$scope.sizingRow, row]) .then(this.setVisibleRows.bind(this)) .then(this.scrollToBottom.bind(this)); @@ -460,17 +451,9 @@ define( * @private */ MCTTableController.prototype.resize = function (rows){ - //Calculate largest row - var largestRow = this.buildLargestRow(rows); - // Has it changed? If so, set the the 'sizing' row which - // determines column widths - if (JSON.stringify(largestRow) !== JSON.stringify(this.$scope.sizingRow)){ - this.$scope.sizingRow = largestRow; - return this.$timeout(this.setElementSizes.bind(this)); - } else { - return fastPromise(undefined); - } + this.$scope.sizingRow = this.buildLargestRow(rows); + return this.$timeout(this.setElementSizes.bind(this)); }; /** From 1bb6e178293b28a046db088550fd8e4d08734556 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 11 Apr 2016 13:30:05 -0700 Subject: [PATCH 08/38] Minor code change to improve clarity --- .../features/table/src/controllers/MCTTableController.js | 7 +++---- platform/features/table/test/TableConfigurationSpec.js | 3 --- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index 9b65613516..1470a9e47c 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -450,8 +450,7 @@ define( * occurred. * @private */ - MCTTableController.prototype.resize = function (rows){ - + MCTTableController.prototype.resize = function (rows) { this.$scope.sizingRow = this.buildLargestRow(rows); return this.$timeout(this.setElementSizes.bind(this)); }; @@ -468,7 +467,7 @@ define( if (this.$scope.enableSort) { displayRows = this.sortRows(displayRows.slice(0)); } - this.$scope.displayRows = displayRows; + return displayRows; }; /** @@ -481,7 +480,7 @@ define( return; } - this.filterAndSort(newRows || []); + this.$scope.displayRows = this.filterAndSort(newRows || []); this.resize(newRows).then(this.setVisibleRows.bind(this)); }; diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js index 79583d09f8..bfc6f50edb 100644 --- a/platform/features/table/test/TableConfigurationSpec.js +++ b/platform/features/table/test/TableConfigurationSpec.js @@ -191,9 +191,6 @@ define( expect(mockTelemetryFormatter.formatRangeValue).toHaveBeenCalled(); }); }); - /** - * TODO: Add test for saving column config - */ }); }); } From 6322964dec2676831a70e23cdf60b30e1da341b5 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 11 Apr 2016 14:09:04 -0700 Subject: [PATCH 09/38] Addressed comments from code review of #822 --- .../src/controllers/MCTTableController.js | 63 ++++++++++--------- .../controllers/MCTTableControllerSpec.js | 33 +++++----- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index 1470a9e47c..fd949f7056 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -286,21 +286,6 @@ define( } }; - /** - * Given a value, if it is a number, or a string representation of a - * number, then return a number representation. Otherwise, return - * the original value. It's a little more robust than using just - * Number() or parseFloat, or isNaN in isolation, all of which are - * fairly inconsistent in their results. - * @param value The value to return as a number. - * @returns {*} The value cast to a Number, or the original value if - * a Number representation is not possible. - */ - MCTTableController.prototype.toNumber = function (value){ - var val = !isNaN(Number(value)) && !isNaN(parseFloat(value)) ? Number(value) : value; - return val; - }; - /** * @private */ @@ -310,17 +295,14 @@ define( sortKey = this.$scope.sortColumn; function binarySearch(searchArray, searchElement, min, max){ - var sampleAt = Math.floor((max - min) / 2) + min, - valA, - valB; + var sampleAt = Math.floor((max - min) / 2) + min; + if (max < min) { return min; // Element is not in array, min gives direction } - valA = self.toNumber(searchElement[sortKey].text); - valB = self.toNumber(searchArray[sampleAt][sortKey].text); - - switch(self.sortComparator(valA, valB)) { + switch(self.sortComparator(searchElement[sortKey].text, + searchArray[sampleAt][sortKey].text)) { case -1: return binarySearch(searchArray, searchElement, min, sampleAt - 1); @@ -358,8 +340,34 @@ define( */ MCTTableController.prototype.sortComparator = function (a, b) { var result = 0, - sortDirectionMultiplier; + sortDirectionMultiplier, + numberA, + numberB; + /** + * Given a value, if it is a number, or a string representation of a + * number, then return a number representation. Otherwise, return + * the original value. It's a little more robust than using just + * Number() or parseFloat, or isNaN in isolation, all of which are + * fairly inconsistent in their results. + * @param value The value to return as a number. + * @returns {*} The value cast to a Number, or the original value if + * a Number representation is not possible. + */ + function toNumber (value){ + var val = !isNaN(Number(value)) && !isNaN(parseFloat(value)) ? Number(value) : value; + return val; + } + numberA = toNumber(a); + numberB = toNumber(b); + + //If they're both numbers, then compare them as numbers + if (typeof numberA === "number" && typeof numberB === "number") { + a = numberA; + b = numberB; + } + + //If they're both strings, then ignore case if (typeof a === "string" && typeof b === "string") { a = a.toLowerCase(); b = b.toLowerCase(); @@ -396,14 +404,7 @@ define( } return rowsToSort.sort(function (a, b) { - //If the values to compare can be compared as - // numbers, do so. String comparison of number - // values can cause inconsistencies - - var valA = self.toNumber(a[sortKey].text), - valB = self.toNumber(b[sortKey].text); - - return self.sortComparator(valA, valB); + return self.sortComparator(a[sortKey].text, b[sortKey].text); }); }; diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 436b53bb16..175d8fed0a 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -180,20 +180,6 @@ define( expect(sortedRows[2].col2.text).toEqual('abc'); }); - it('converts number strings to numbers', function () { - var val1 = "", - val2 = "1", - val3 = "2016-04-05 18:41:30.713Z", - val4 = "1.1", - val5 = "8.945520958175627e-13"; - - expect(controller.toNumber(val1)).toEqual(""); - expect(controller.toNumber(val2)).toEqual(1); - expect(controller.toNumber(val3)).toEqual("2016-04-05 18:41:30.713Z"); - expect(controller.toNumber(val4)).toEqual(1.1); - expect(controller.toNumber(val5)).toEqual(8.945520958175627e-13); - }); - it('correctly sorts rows of differing types', function () { mockScope.sortColumn = 'col2'; mockScope.sortDirection = 'desc'; @@ -224,7 +210,24 @@ define( expect(sortedRows[sortedRows.length-1].col2.text).toEqual(''); }); - describe('Adding new rows', function() { + describe('The sort comparator', function () { + it('Correctly sorts different data types', function () { + var val1 = "", + val2 = "1", + val3 = "2016-04-05 18:41:30.713Z", + val4 = "1.1", + val5 = "8.945520958175627e-13"; + mockScope.sortDirection = "asc"; + + expect(controller.sortComparator(val1, val2)).toEqual(-1); + expect(controller.sortComparator(val3, val1)).toEqual(1); + expect(controller.sortComparator(val3, val2)).toEqual(1); + expect(controller.sortComparator(val4, val2)).toEqual(1); + expect(controller.sortComparator(val2, val5)).toEqual(1); + }); + }); + + describe('Adding new rows', function () { var row4, row5, row6; From 7f108c3b24cffc4556df7796519633c7fac6efe5 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 22 Mar 2016 20:17:23 -0700 Subject: [PATCH 10/38] [Edit Mode] #635 Removed Edit-related concerns from ContextMenuGesture --- platform/commonUI/edit/bundle.js | 7 ++ .../edit/src/policies/EditActionPolicy.js | 11 +-- .../policies/EditContextualActionPolicy.js | 59 ++++++++++++++ .../test/policies/EditActionPolicySpec.js | 73 +++++++++++++----- .../EditContextualActionPolicySpec.js | 76 +++++++++++++++++++ platform/core/src/actions/ActionCapability.js | 44 +++-------- .../core/test/actions/ActionCapabilitySpec.js | 7 +- platform/representation/bundle.js | 4 +- .../src/gestures/ContextMenuGesture.js | 30 ++------ .../test/gestures/ContextMenuGestureSpec.js | 14 +--- 10 files changed, 225 insertions(+), 100 deletions(-) create mode 100644 platform/commonUI/edit/src/policies/EditContextualActionPolicy.js create mode 100644 platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index 695ca2aab3..053c2a3d83 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -37,6 +37,7 @@ define([ "./src/policies/EditableLinkPolicy", "./src/policies/EditableMovePolicy", "./src/policies/EditNavigationPolicy", + "./src/policies/EditContextualActionPolicy", "./src/representers/EditRepresenter", "./src/representers/EditToolbarRepresenter", "text!./res/templates/library.html", @@ -61,6 +62,7 @@ define([ EditableLinkPolicy, EditableMovePolicy, EditNavigationPolicy, + EditContextualActionPolicy, EditRepresenter, EditToolbarRepresenter, libraryTemplate, @@ -191,6 +193,11 @@ define([ "category": "action", "implementation": EditActionPolicy }, + { + "category": "action", + "implementation": EditContextualActionPolicy, + "depends": ["navigationService"] + }, { "category": "action", "implementation": EditableMovePolicy diff --git a/platform/commonUI/edit/src/policies/EditActionPolicy.js b/platform/commonUI/edit/src/policies/EditActionPolicy.js index 7f623ce07b..502577ade9 100644 --- a/platform/commonUI/edit/src/policies/EditActionPolicy.js +++ b/platform/commonUI/edit/src/policies/EditActionPolicy.js @@ -38,14 +38,6 @@ define( this.policyService = policyService; } - function applicableView(key){ - return ['plot', 'scrolling'].indexOf(key) >= 0; - } - - function editableType(key){ - return key === 'telemetry.panel'; - } - /** * Get a count of views which are not flagged as non-editable. * @private @@ -65,7 +57,8 @@ define( // A view is editable unless explicitly flagged as not (views || []).forEach(function (view) { - if (view.editable === true || (applicableView(view.key) && editableType(type.getKey()))) { + if (view.editable === true || + (view.key === 'plot' && type.getKey() === 'telemetry.panel')) { count++; } }); diff --git a/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js b/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js new file mode 100644 index 0000000000..9f98698439 --- /dev/null +++ b/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js @@ -0,0 +1,59 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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. + *****************************************************************************/ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Policy controlling whether the context menu is visible when + * objects are being edited + * @memberof platform/commonUI/edit + * @constructor + * @implements {Policy.} + */ + function EditContextualActionPolicy(navigationService) { + this.navigationService = navigationService; + this.blacklist = ["move", "copy", "link", "window", "follow"]; + } + + EditContextualActionPolicy.prototype.allow = function (action, context) { + var selectedObject = context.domainObject, + navigatedObject = this.navigationService.getNavigation(), + actionMetadata = action.getMetadata ? action.getMetadata() : {}; + + if (navigatedObject.hasCapability('editor')) { + if (!selectedObject.hasCapability('editor')){ + return false; + } else { + return this.blacklist.indexOf(actionMetadata.key) === -1; + } + } else { + return true; + } + }; + + return EditContextualActionPolicy; + } +); diff --git a/platform/commonUI/edit/test/policies/EditActionPolicySpec.js b/platform/commonUI/edit/test/policies/EditActionPolicySpec.js index 823c3d2737..e2cf51ab97 100644 --- a/platform/commonUI/edit/test/policies/EditActionPolicySpec.js +++ b/platform/commonUI/edit/test/policies/EditActionPolicySpec.js @@ -35,19 +35,43 @@ define( mockDomainObject, mockEditAction, mockPropertiesAction, + mockTypeCapability, + mockStatusCapability, + capabilities, + plotView, policy; beforeEach(function () { mockDomainObject = jasmine.createSpyObj( 'domainObject', - [ 'useCapability' ] + [ + 'useCapability', + 'hasCapability', + 'getCapability' + ] ); + mockStatusCapability = jasmine.createSpyObj('statusCapability', ['get']); + mockStatusCapability.get.andReturn(false); + mockTypeCapability = jasmine.createSpyObj('type', ['getKey']); + capabilities = { + 'status': mockStatusCapability, + 'type': mockTypeCapability + }; + mockEditAction = jasmine.createSpyObj('edit', ['getMetadata']); mockPropertiesAction = jasmine.createSpyObj('edit', ['getMetadata']); + mockDomainObject.getCapability.andCallFake(function(capability){ + return capabilities[capability]; + }); + mockDomainObject.hasCapability.andCallFake(function(capability){ + return !!capabilities[capability]; + }); + editableView = { editable: true }; nonEditableView = { editable: false }; undefinedView = { someKey: "some value" }; + plotView = { key: "plot", editable: false }; testViews = []; mockDomainObject.useCapability.andCallFake(function (c) { @@ -66,38 +90,53 @@ define( policy = new EditActionPolicy(); }); - //TODO: Disabled for NEM Beta - xit("allows the edit action when there are editable views", function () { + it("allows the edit action when there are editable views", function () { testViews = [ editableView ]; - expect(policy.allow(mockEditAction, testContext)).toBeTruthy(); - // No edit flag defined; should be treated as editable - testViews = [ undefinedView, undefinedView ]; - expect(policy.allow(mockEditAction, testContext)).toBeTruthy(); + expect(policy.allow(mockEditAction, testContext)).toBe(true); }); - //TODO: Disabled for NEM Beta - xit("allows the edit properties action when there are no editable views", function () { + it("allows the edit properties action when there are no editable views", function () { testViews = [ nonEditableView, nonEditableView ]; - expect(policy.allow(mockPropertiesAction, testContext)).toBeTruthy(); + expect(policy.allow(mockPropertiesAction, testContext)).toBe(true); }); - //TODO: Disabled for NEM Beta - xit("disallows the edit action when there are no editable views", function () { + it("disallows the edit action when there are no editable views", function () { testViews = [ nonEditableView, nonEditableView ]; - expect(policy.allow(mockEditAction, testContext)).toBeFalsy(); + expect(policy.allow(mockEditAction, testContext)).toBe(false); }); - //TODO: Disabled for NEM Beta - xit("disallows the edit properties action when there are" + + it("disallows the edit properties action when there are" + " editable views", function () { testViews = [ editableView ]; - expect(policy.allow(mockPropertiesAction, testContext)).toBeFalsy(); + expect(policy.allow(mockPropertiesAction, testContext)).toBe(false); }); + it("disallows the edit action when object is already being" + + " edited", function () { + testViews = [ editableView ]; + mockStatusCapability.get.andReturn(true); + expect(policy.allow(mockEditAction, testContext)).toBe(false); + }); + + it("allows editing of panels in plot view", function () { + testViews = [ plotView ]; + mockTypeCapability.getKey.andReturn('telemetry.panel'); + + expect(policy.allow(mockEditAction, testContext)).toBe(true); + }); + + it("disallows editing of plot view when object not a panel type", function () { + testViews = [ plotView ]; + mockTypeCapability.getKey.andReturn('something.else'); + + expect(policy.allow(mockEditAction, testContext)).toBe(false); + }); + + it("allows the edit properties outside of the 'view-control' category", function () { testViews = [ nonEditableView ]; testContext.category = "something-else"; - expect(policy.allow(mockPropertiesAction, testContext)).toBeTruthy(); + expect(policy.allow(mockPropertiesAction, testContext)).toBe(true); }); }); } diff --git a/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js b/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js new file mode 100644 index 0000000000..625cd13b18 --- /dev/null +++ b/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js @@ -0,0 +1,76 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,jasmine,xit,xdescribe*/ + +define( + ["../../src/policies/EditContextualActionPolicy"], + function (EditContextualActionPolicy) { + "use strict"; + + describe("The Edit contextual action policy", function () { + var policy, + navigationService, + mockAction, + context, + navigatedObject, + mockDomainObject, + metadata; + + beforeEach(function () { + navigatedObject = jasmine.createSpyObj("navigatedObject", ["hasCapability"]); + navigatedObject.hasCapability.andReturn(false); + + mockDomainObject = jasmine.createSpyObj("domainObject", ["hasCapability"]); + mockDomainObject.hasCapability.andReturn(false); + + navigationService = jasmine.createSpyObj("navigationService", ["getNavigation"]); + navigationService.getNavigation.andReturn(navigatedObject); + + metadata = {key: "move"}; + mockAction = jasmine.createSpyObj("action", ["getMetadata"]); + mockAction.getMetadata.andReturn(metadata); + + context = {domainObject: mockDomainObject}; + + policy = new EditContextualActionPolicy(navigationService); + }); + + it('Allows all actions when navigated object not in edit mode', function() { + expect(policy.allow(mockAction, context)).toBe(true); + }); + + it('Allows no actions when navigated object in edit mode, but' + + ' selected object not in edit mode ', function() { + navigatedObject.hasCapability.andReturn(true); + expect(policy.allow(mockAction, context)).toBe(false); + }); + + it('Disallows blacklisted actions when navigated object and' + + ' selected object in edit mode', function() { + navigatedObject.hasCapability.andReturn(true); + mockDomainObject.hasCapability.andReturn(true); + expect(policy.allow(mockAction, context)).toBe(false); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/core/src/actions/ActionCapability.js b/platform/core/src/actions/ActionCapability.js index 7caf5c0ecc..2164969a05 100644 --- a/platform/core/src/actions/ActionCapability.js +++ b/platform/core/src/actions/ActionCapability.js @@ -28,7 +28,6 @@ define( [], function () { "use strict"; - var DISALLOWED_ACTIONS = ["copy", "window", "follow"]; /** * The ActionCapability allows applicable Actions to be retrieved and @@ -55,37 +54,22 @@ define( this.domainObject = domainObject; } - function isEditable(domainObject){ - return domainObject.getCapability('status').get('editing'); - } - - function hasEditableAncestor(domainObject){ - return domainObject.hasCapability('context') && - domainObject - .getCapability('context') - .getPath() - .some(function isEditable (ancestor){ - return ancestor.getCapability('status').get('editing'); - }); - } - /** - * Retrieve the actions applicable to the domain object in the given - * context. + * Perform an action. This will find and perform the + * first matching action available for the specified + * context or key. * * @param {ActionContext|string} context the context in which - * to assess the applicability of the available actions; this is - * passed along to the action service to match against available + * to perform the action; this is passed along to + * the action service to match against available * actions. The "domainObject" field will automatically * be populated with the domain object that exposed * this capability. If given as a string, this will * be taken as the "key" field to match against * specific actions. - * - * Additionally, this function will limit the actions - * available for an object in Edit Mode - * @returns {Array} The actions applicable to this domain - * object in the given context + * @returns {Promise} the result of the action that was + * performed, or undefined if no matching action + * was found. * @memberof platform/core.ActionCapability# */ ActionCapability.prototype.getActions = function (context) { @@ -94,19 +78,11 @@ define( // but additionally adds a domainObject field. var baseContext = typeof context === 'string' ? { key: context } : (context || {}), - actionContext = Object.create(baseContext), - actions; + actionContext = Object.create(baseContext); actionContext.domainObject = this.domainObject; - actions = this.actionService.getActions(actionContext) || []; - if (isEditable(this.domainObject) || hasEditableAncestor(this.domainObject)){ - return actions.filter(function(action){ - return DISALLOWED_ACTIONS.indexOf(action.getMetadata().key) === -1; - }); - } else { - return actions; - } + return this.actionService.getActions(actionContext); }; /** diff --git a/platform/core/test/actions/ActionCapabilitySpec.js b/platform/core/test/actions/ActionCapabilitySpec.js index feb1273721..ab3db012f1 100644 --- a/platform/core/test/actions/ActionCapabilitySpec.js +++ b/platform/core/test/actions/ActionCapabilitySpec.js @@ -19,8 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -/*global define,Promise,describe,xdescribe,it,expect,beforeEach,waitsFor, - jasmine*/ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ /** * ActionCapabilitySpec. Created by vwoeltje on 11/6/14. @@ -29,8 +28,8 @@ define( ["../../src/actions/ActionCapability"], function (ActionCapability) { "use strict"; - //TODO: Disabled for NEM beta - xdescribe("The action capability", function () { + + describe("The action capability", function () { var mockQ, mockAction, mockActionService, diff --git a/platform/representation/bundle.js b/platform/representation/bundle.js index a58ce4f34c..c257e972f0 100644 --- a/platform/representation/bundle.js +++ b/platform/representation/bundle.js @@ -99,9 +99,7 @@ define([ "implementation": ContextMenuGesture, "depends": [ "$timeout", - "$parse", - "agentService", - "navigationService" + "agentService" ] } ], diff --git a/platform/representation/src/gestures/ContextMenuGesture.js b/platform/representation/src/gestures/ContextMenuGesture.js index be2fb27e91..e7c0c7ba9f 100644 --- a/platform/representation/src/gestures/ContextMenuGesture.js +++ b/platform/representation/src/gestures/ContextMenuGesture.js @@ -41,32 +41,16 @@ define( * in the context menu will be performed * @implements {Gesture} */ - function ContextMenuGesture($timeout, $parse, agentService, navigationService, element, domainObject) { + function ContextMenuGesture($timeout, agentService, element, domainObject) { var isPressing, - longTouchTime = 500, - parameters = element && element.attr('parameters') && $parse(element.attr('parameters'))(); - - function suppressMenu() { - return parameters - && parameters.suppressMenuOnEdit - && navigationService.getNavigation() - && navigationService.getNavigation().hasCapability('editor'); - } + longTouchTime = 500; function showMenu(event) { - /** - * Some menu items should have the context menu action - * suppressed (eg. the navigation menu on the left) - */ - if (suppressMenu()){ - return; - } else { - domainObject.getCapability('action').perform({ - key: 'menu', - domainObject: domainObject, - event: event - }); - } + domainObject.getCapability('action').perform({ + key: 'menu', + domainObject: domainObject, + event: event + }); } // When context menu event occurs, show object actions instead diff --git a/platform/representation/test/gestures/ContextMenuGestureSpec.js b/platform/representation/test/gestures/ContextMenuGestureSpec.js index 1ed9d628ec..0e78729ef9 100644 --- a/platform/representation/test/gestures/ContextMenuGestureSpec.js +++ b/platform/representation/test/gestures/ContextMenuGestureSpec.js @@ -30,16 +30,14 @@ define( function (ContextMenuGesture) { "use strict"; - var JQLITE_FUNCTIONS = [ "on", "off", "find", "append", "remove", "attr" ], + var JQLITE_FUNCTIONS = [ "on", "off", "find", "append", "remove" ], DOMAIN_OBJECT_METHODS = [ "getId", "getModel", "getCapability", "hasCapability", "useCapability"]; describe("The 'context menu' gesture", function () { var mockTimeout, - mockParse, mockElement, mockAgentService, - mockNavigationService, mockDomainObject, mockEvent, mockTouchEvent, @@ -53,7 +51,6 @@ define( beforeEach(function () { mockTimeout = jasmine.createSpy("$timeout"); - mockParse = jasmine.createSpy("$parse"); mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS); mockAgentService = jasmine.createSpyObj("agentService", ["isMobile"]); mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS); @@ -62,17 +59,14 @@ define( "action", [ "perform", "getActions" ] ); - mockActionContext = jasmine.createSpyObj( - "actionContext", - [ "" ] - ); mockActionContext = {domainObject: mockDomainObject, event: mockEvent}; mockDomainObject.getCapability.andReturn(mockContextMenuAction); mockContextMenuAction.perform.andReturn(jasmine.any(Function)); mockAgentService.isMobile.andReturn(false); - gesture = new ContextMenuGesture(mockTimeout, mockParse, mockAgentService, mockNavigationService, mockElement, mockDomainObject); + + gesture = new ContextMenuGesture(mockTimeout, mockAgentService, mockElement, mockDomainObject); // Capture the contextmenu callback fireGesture = mockElement.on.mostRecentCall.args[1]; @@ -108,7 +102,7 @@ define( mockAgentService.isMobile.andReturn(true); // Then create new (mobile) gesture - gesture = new ContextMenuGesture(mockTimeout, mockParse, mockAgentService, mockNavigationService, mockElement, mockDomainObject); + gesture = new ContextMenuGesture(mockTimeout, mockAgentService, mockElement, mockDomainObject); // Set calls for the touchstart and touchend gestures fireTouchStartGesture = mockElement.on.calls[1].args[1]; From 2fe7ba982fd028a88c0bace7a81ac76e129b21dd Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 11 Apr 2016 16:14:51 -0700 Subject: [PATCH 11/38] Added 'Open in New Tab' to context menu in edit mode --- .../edit/src/policies/EditContextualActionPolicy.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js b/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js index 9f98698439..06fe9de640 100644 --- a/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js +++ b/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js @@ -35,7 +35,11 @@ define( */ function EditContextualActionPolicy(navigationService) { this.navigationService = navigationService; - this.blacklist = ["move", "copy", "link", "window", "follow"]; + //The list of objects disallowed on target object when in edit mode + this.editBlacklist = ["copy", "follow", "window"]; + //The list of objects disallowed on target object that is not in + // edit mode (ie. the context menu in the tree on the LHS). + this.nonEditBlacklist = ["copy", "follow", "properties", "move", "link", "remove"]; } EditContextualActionPolicy.prototype.allow = function (action, context) { @@ -45,9 +49,10 @@ define( if (navigatedObject.hasCapability('editor')) { if (!selectedObject.hasCapability('editor')){ - return false; + //Target is in the context menu + return this.nonEditBlacklist.indexOf(actionMetadata.key) === -1; } else { - return this.blacklist.indexOf(actionMetadata.key) === -1; + return this.editBlacklist.indexOf(actionMetadata.key) === -1; } } else { return true; From 519a9333ab20b3c8822f9b19d6436819bfdf225a Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 11 Apr 2016 17:10:35 -0700 Subject: [PATCH 12/38] Updated tests --- .../policies/EditContextualActionPolicySpec.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js b/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js index 625cd13b18..8dbf2c128c 100644 --- a/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js +++ b/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js @@ -58,16 +58,25 @@ define( expect(policy.allow(mockAction, context)).toBe(true); }); - it('Allows no actions when navigated object in edit mode, but' + - ' selected object not in edit mode ', function() { + it('Allows "window" action when navigated object in edit mode,' + + ' but selected object not in edit mode ', function() { navigatedObject.hasCapability.andReturn(true); + metadata.key = "window"; + expect(policy.allow(mockAction, context)).toBe(true); + }); + + it('Disallows "move" action when navigated object in edit mode,' + + ' but selected object not in edit mode ', function() { + navigatedObject.hasCapability.andReturn(true); + metadata.key = "move"; expect(policy.allow(mockAction, context)).toBe(false); }); - it('Disallows blacklisted actions when navigated object and' + + it('Disallows copy action when navigated object and' + ' selected object in edit mode', function() { navigatedObject.hasCapability.andReturn(true); mockDomainObject.hasCapability.andReturn(true); + metadata.key = "copy"; expect(policy.allow(mockAction, context)).toBe(false); }); From 1d78af8f1d9fa34e40c35dcda5f38aa930172fc0 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 12 Apr 2016 13:02:48 -0700 Subject: [PATCH 13/38] [Build] Fix build on Windows * In the prepublish step, run bower and gulp via node, instead of relying on shebang interpretation. (Forward-slash path separators appear to get normalized by node itself before executing the scripts.) * In the gulp build, replace hard-coded *nix-style separators with path.sep; this allows stylesheets to be output to expected locations when building on Windows. Addresses #827. --- gulpfile.js | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 0df0aa2ae6..8121e2b146 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -91,7 +91,8 @@ gulp.task('stylesheets', function () { .pipe(sourcemaps.init()) .pipe(sass(options.sass).on('error', sass.logError)) .pipe(rename(function (file) { - file.dirname = file.dirname.replace('/sass', '/css'); + file.dirname = + file.dirname.replace(path.sep + 'sass', path.sep + 'css'); return file; })) .pipe(sourcemaps.write('.')) diff --git a/package.json b/package.json index b7b933a680..6830d197e6 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "jsdoc": "jsdoc -c jsdoc.json -r -d target/docs/api", "otherdoc": "node docs/gendocs.js --in docs/src --out target/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", "docs": "npm run jsdoc ; npm run otherdoc", - "prepublish": "./node_modules/bower/bin/bower install && ./node_modules/gulp/bin/gulp.js install" + "prepublish": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install" }, "repository": { "type": "git", From 26e368f52dc071e17d7ed8dba79834bc53d8f6bd Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 12 Apr 2016 14:40:19 -0700 Subject: [PATCH 14/38] [Edit mode] Simplify SaveAction --- .../browse/src/creation/CreateAction.js | 2 +- platform/commonUI/edit/bundle.js | 11 ++ .../commonUI/edit/src/actions/SaveAction.js | 86 +-------- .../commonUI/edit/src/actions/SaveAsAction.js | 169 +++++++++++++++++ .../edit/test/actions/SaveActionSpec.js | 65 ++++--- .../edit/test/actions/SaveAsActionSpec.js | 174 ++++++++++++++++++ .../general/res/sass/controls/_buttons.scss | 5 + 7 files changed, 398 insertions(+), 114 deletions(-) create mode 100644 platform/commonUI/edit/src/actions/SaveAsAction.js create mode 100644 platform/commonUI/edit/test/actions/SaveAsActionSpec.js diff --git a/platform/commonUI/browse/src/creation/CreateAction.js b/platform/commonUI/browse/src/creation/CreateAction.js index 83d88ba709..dd8f227446 100644 --- a/platform/commonUI/browse/src/creation/CreateAction.js +++ b/platform/commonUI/browse/src/creation/CreateAction.js @@ -103,7 +103,7 @@ define( if (countEditableViews(editableObject) > 0 && editableObject.hasCapability('composition')) { this.navigationService.setNavigation(editableObject); } else { - return editableObject.getCapability('action').perform('save'); + return editableObject.getCapability('action').perform('save-as'); } }; diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index 695ca2aab3..7c8f45eb81 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -32,6 +32,7 @@ define([ "./src/actions/PropertiesAction", "./src/actions/RemoveAction", "./src/actions/SaveAction", + "./src/actions/SaveAsAction", "./src/actions/CancelAction", "./src/policies/EditActionPolicy", "./src/policies/EditableLinkPolicy", @@ -56,6 +57,7 @@ define([ PropertiesAction, RemoveAction, SaveAction, + SaveAsAction, CancelAction, EditActionPolicy, EditableLinkPolicy, @@ -165,6 +167,15 @@ define([ "implementation": SaveAction, "name": "Save", "description": "Save changes made to these objects.", + "depends": [], + "priority": "mandatory" + }, + { + "key": "save-as", + "category": "conclude-editing", + "implementation": SaveAsAction, + "name": "Save As", + "description": "Save changes made to these objects.", "depends": [ "$injector", "policyService", diff --git a/platform/commonUI/edit/src/actions/SaveAction.js b/platform/commonUI/edit/src/actions/SaveAction.js index 1415a6ed4b..ff2a0997fb 100644 --- a/platform/commonUI/edit/src/actions/SaveAction.js +++ b/platform/commonUI/edit/src/actions/SaveAction.js @@ -24,8 +24,8 @@ define( - ['../../../browse/src/creation/CreateWizard'], - function (CreateWizard) { + [], + function () { 'use strict'; /** @@ -37,31 +37,11 @@ define( * @memberof platform/commonUI/edit */ function SaveAction( - $injector, - policyService, - dialogService, - creationService, - copyService, context ) { this.domainObject = (context || {}).domainObject; - this.injectObjectService = function(){ - this.objectService = $injector.get("objectService"); - }; - this.policyService = policyService; - this.dialogService = dialogService; - this.creationService = creationService; - this.copyService = copyService; } - SaveAction.prototype.getObjectService = function(){ - // Lazily acquire object service (avoids cyclical dependency) - if (!this.objectService) { - this.injectObjectService(); - } - return this.objectService; - }; - /** * Save changes and conclude editing. * @@ -70,9 +50,7 @@ define( * @memberof platform/commonUI/edit.SaveAction# */ SaveAction.prototype.perform = function () { - var domainObject = this.domainObject, - copyService = this.copyService, - self = this; + var domainObject = this.domainObject; function resolveWith(object){ return function () { @@ -80,64 +58,13 @@ define( }; } - function doWizardSave(parent) { - var context = domainObject.getCapability("context"), - wizard = new CreateWizard( - domainObject, - parent, - self.policyService - ); - - return self.dialogService - .getUserInput( - wizard.getFormStructure(true), - wizard.getInitialFormValue() - ) - .then(wizard.populateObjectFromInput.bind(wizard)); - } - - function fetchObject(objectId){ - return self.getObjectService().getObjects([objectId]).then(function(objects){ - return objects[objectId]; - }); - } - - function getParent(object){ - return fetchObject(object.getModel().location); - } - - function allowClone(objectToClone) { - return (objectToClone.getId() === domainObject.getId()) || - objectToClone.getCapability('location').isOriginal(); - } - - function cloneIntoParent(parent) { - return copyService.perform(domainObject, parent, allowClone); - } - - function cancelEditingAfterClone(clonedObject) { - return domainObject.getCapability("editor").cancel() - .then(resolveWith(clonedObject)); - } - // Invoke any save behavior introduced by the editor capability; // this is introduced by EditableDomainObject which is // used to insulate underlying objects from changes made // during editing. function doSave() { - //This is a new 'virtual object' that has not been persisted - // yet. - if (domainObject.getModel().persisted === undefined){ - return getParent(domainObject) - .then(doWizardSave) - .then(getParent) - .then(cloneIntoParent) - .then(cancelEditingAfterClone) - .catch(resolveWith(false)); - } else { - return domainObject.getCapability("editor").save() - .then(resolveWith(domainObject.getOriginalObject())); - } + return domainObject.getCapability("editor").save() + .then(resolveWith(domainObject.getOriginalObject())); } // Discard the current root view (which will be the editing @@ -162,7 +89,8 @@ define( SaveAction.appliesTo = function (context) { var domainObject = (context || {}).domainObject; return domainObject !== undefined && - domainObject.hasCapability("editor"); + domainObject.hasCapability("editor") && + domainObject.getModel().persisted !== undefined; }; return SaveAction; diff --git a/platform/commonUI/edit/src/actions/SaveAsAction.js b/platform/commonUI/edit/src/actions/SaveAsAction.js new file mode 100644 index 0000000000..bfd723c6d5 --- /dev/null +++ b/platform/commonUI/edit/src/actions/SaveAsAction.js @@ -0,0 +1,169 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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. + *****************************************************************************/ +/*global define*/ +/*jslint es5: true */ + + +define( + ['../../../browse/src/creation/CreateWizard'], + function (CreateWizard) { + 'use strict'; + + /** + * The "Save" action; the action triggered by clicking Save from + * Edit Mode. Exits the editing user interface and invokes object + * capabilities to persist the changes that have been made. + * @constructor + * @implements {Action} + * @memberof platform/commonUI/edit + */ + function SaveAsAction( + $injector, + policyService, + dialogService, + creationService, + copyService, + context + ) { + this.domainObject = (context || {}).domainObject; + this.injectObjectService = function(){ + this.objectService = $injector.get("objectService"); + }; + this.policyService = policyService; + this.dialogService = dialogService; + this.creationService = creationService; + this.copyService = copyService; + } + + /** + * @private + */ + SaveAsAction.prototype.createWizard = function (parent) { + return new CreateWizard( + this.domainObject, + parent, + this.policyService + ); + }; + + /** + * @private + */ + SaveAsAction.prototype.getObjectService = function(){ + // Lazily acquire object service (avoids cyclical dependency) + if (!this.objectService) { + this.injectObjectService(); + } + return this.objectService; + }; + + function resolveWith(object){ + return function () { + return object; + }; + } + + /** + * Save changes and conclude editing. + * + * @returns {Promise} a promise that will be fulfilled when + * cancellation has completed + * @memberof platform/commonUI/edit.SaveAction# + */ + SaveAsAction.prototype.perform = function () { + // Discard the current root view (which will be the editing + // UI, which will have been pushed atop the Browse UI.) + function returnToBrowse(object) { + if (object) { + object.getCapability("action").perform("navigate"); + } + return object; + } + + return this.save().then(returnToBrowse); + }; + + /** + * @private + */ + SaveAsAction.prototype.save = function () { + var self = this, + domainObject = this.domainObject, + copyService = this.copyService; + + function doWizardSave(parent) { + var wizard = self.createWizard(parent); + + return self.dialogService + .getUserInput(wizard.getFormStructure(true), + wizard.getInitialFormValue() + ).then(wizard.populateObjectFromInput.bind(wizard)); + } + + function fetchObject(objectId){ + return self.getObjectService().getObjects([objectId]).then(function(objects){ + return objects[objectId]; + }); + } + + function getParent(object){ + return fetchObject(object.getModel().location); + } + + function allowClone(objectToClone) { + return (objectToClone.getId() === domainObject.getId()) || + objectToClone.getCapability('location').isOriginal(); + } + + function cloneIntoParent(parent) { + return copyService.perform(domainObject, parent, allowClone); + } + + function cancelEditingAfterClone(clonedObject) { + return domainObject.getCapability("editor").cancel() + .then(resolveWith(clonedObject)); + } + + return getParent(domainObject) + .then(doWizardSave) + .then(getParent) + .then(cloneIntoParent) + .then(cancelEditingAfterClone) + .catch(resolveWith(false)); + }; + + /** + * Check if this action is applicable in a given context. + * This will ensure that a domain object is present in the context, + * and that this domain object is in Edit mode. + * @returns true if applicable + */ + SaveAsAction.appliesTo = function (context) { + var domainObject = (context || {}).domainObject; + return domainObject !== undefined && + domainObject.hasCapability("editor") && + domainObject.getModel().persisted === undefined; + }; + + return SaveAsAction; + } +); diff --git a/platform/commonUI/edit/test/actions/SaveActionSpec.js b/platform/commonUI/edit/test/actions/SaveActionSpec.js index 656d6e1ebc..98279e39b1 100644 --- a/platform/commonUI/edit/test/actions/SaveActionSpec.js +++ b/platform/commonUI/edit/test/actions/SaveActionSpec.js @@ -27,11 +27,11 @@ define( "use strict"; describe("The Save action", function () { - var mockLocation, - mockDomainObject, + var mockDomainObject, mockEditorCapability, - mockUrlService, actionContext, + mockActionCapability, + capabilities = {}, action; function mockPromise(value) { @@ -43,65 +43,62 @@ define( } beforeEach(function () { - mockLocation = jasmine.createSpyObj( - "$location", - [ "path" ] - ); mockDomainObject = jasmine.createSpyObj( "domainObject", - [ "getCapability", "hasCapability" ] + [ + "getCapability", + "hasCapability", + "getModel", + "getOriginalObject" + ] ); mockEditorCapability = jasmine.createSpyObj( "editor", [ "save", "cancel" ] ); - mockUrlService = jasmine.createSpyObj( - "urlService", - ["urlForLocation"] + mockActionCapability = jasmine.createSpyObj( + "actionCapability", + [ "perform"] ); - + capabilities.editor = mockEditorCapability; + capabilities.action = mockActionCapability; actionContext = { domainObject: mockDomainObject }; mockDomainObject.hasCapability.andReturn(true); - mockDomainObject.getCapability.andReturn(mockEditorCapability); + mockDomainObject.getCapability.andCallFake(function (capability) { + return capabilities[capability]; + }); + mockDomainObject.getModel.andReturn({persisted: 0}); mockEditorCapability.save.andReturn(mockPromise(true)); + mockDomainObject.getOriginalObject.andReturn(mockDomainObject); - action = new SaveAction(mockLocation, mockUrlService, actionContext); + action = new SaveAction(actionContext); }); it("only applies to domain object with an editor capability", function () { - expect(SaveAction.appliesTo(actionContext)).toBeTruthy(); + expect(SaveAction.appliesTo(actionContext)).toBe(true); expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor"); mockDomainObject.hasCapability.andReturn(false); mockDomainObject.getCapability.andReturn(undefined); - expect(SaveAction.appliesTo(actionContext)).toBeFalsy(); + expect(SaveAction.appliesTo(actionContext)).toBe(false); }); - //TODO: Disabled for NEM Beta - xit("invokes the editor capability's save functionality when performed", function () { - // Verify precondition - expect(mockEditorCapability.save).not.toHaveBeenCalled(); - action.perform(); - - // Should have called cancel - expect(mockEditorCapability.save).toHaveBeenCalled(); - - // Also shouldn't call cancel - expect(mockEditorCapability.cancel).not.toHaveBeenCalled(); + it("only applies to domain object that has already been persisted", + function () { + mockDomainObject.getModel.andReturn({persisted: undefined}); + expect(SaveAction.appliesTo(actionContext)).toBe(false); }); - //TODO: Disabled for NEM Beta - xit("returns to browse when performed", function () { - action.perform(); - expect(mockLocation.path).toHaveBeenCalledWith( - mockUrlService.urlForLocation("browse", mockDomainObject) - ); - }); + it("uses the editor capability to save the object", + function () { + action.perform(); + expect(mockEditorCapability.save).toHaveBeenCalled(); + }); }); } ); \ No newline at end of file diff --git a/platform/commonUI/edit/test/actions/SaveAsActionSpec.js b/platform/commonUI/edit/test/actions/SaveAsActionSpec.js new file mode 100644 index 0000000000..866447a874 --- /dev/null +++ b/platform/commonUI/edit/test/actions/SaveAsActionSpec.js @@ -0,0 +1,174 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,jasmine,xit,xdescribe*/ + +define( + ["../../src/actions/SaveAsAction"], + function (SaveAsAction) { + "use strict"; + + describe("The Save As action", function () { + var mockDomainObject, + mockEditorCapability, + mockActionCapability, + mockObjectService, + mockDialogService, + mockCopyService, + mockParent, + mockUrlService, + actionContext, + capabilities = {}, + action; + + function noop () {} + + function mockPromise(value) { + return (value || {}).then ? value : + { + then: function (callback) { + return mockPromise(callback(value)); + }, + catch: function (callback) { + return mockPromise(callback(value)); + } + } ; + } + + beforeEach(function () { + mockDomainObject = jasmine.createSpyObj( + "domainObject", + [ + "getCapability", + "hasCapability", + "getModel" + ] + ); + mockDomainObject.hasCapability.andReturn(true); + mockDomainObject.getCapability.andCallFake(function (capability) { + return capabilities[capability]; + }); + mockDomainObject.getModel.andReturn({location: 'a', persisted: undefined}); + + mockParent = jasmine.createSpyObj( + "parentObject", + [ + "getCapability", + "hasCapability", + "getModel" + ] + ); + + mockEditorCapability = jasmine.createSpyObj( + "editor", + [ "save", "cancel" ] + ); + mockEditorCapability.cancel.andReturn(mockPromise(undefined)); + mockEditorCapability.save.andReturn(mockPromise(true)); + capabilities.editor = mockEditorCapability; + + mockActionCapability = jasmine.createSpyObj( + "action", + ["perform"] + ); + capabilities.action = mockActionCapability; + + mockObjectService = jasmine.createSpyObj( + "objectService", + ["getObjects"] + ); + mockObjectService.getObjects.andReturn(mockPromise({'a': mockParent})); + + mockDialogService = jasmine.createSpyObj( + "dialogService", + [ + "getUserInput" + ] + ); + mockDialogService.getUserInput.andReturn(mockPromise(undefined)); + + mockCopyService = jasmine.createSpyObj( + "copyService", + [ + "perform" + ] + ); + + mockUrlService = jasmine.createSpyObj( + "urlService", + ["urlForLocation"] + ); + + actionContext = { + domainObject: mockDomainObject + }; + + action = new SaveAsAction(undefined, undefined, mockDialogService, undefined, mockCopyService, actionContext); + + spyOn(action, "getObjectService"); + action.getObjectService.andReturn(mockObjectService); + + spyOn(action, "createWizard"); + action.createWizard.andReturn({ + getFormStructure: noop, + getInitialFormValue: noop, + populateObjectFromInput: function() { + return mockDomainObject; + } + }); + + }); + + it("only applies to domain object with an editor capability", function () { + expect(SaveAsAction.appliesTo(actionContext)).toBe(true); + expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor"); + + mockDomainObject.hasCapability.andReturn(false); + mockDomainObject.getCapability.andReturn(undefined); + expect(SaveAsAction.appliesTo(actionContext)).toBe(false); + }); + + it("only applies to domain object that has not already been" + + " persisted", function () { + expect(SaveAsAction.appliesTo(actionContext)).toBe(true); + expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor"); + + mockDomainObject.getModel.andReturn({persisted: 0}); + expect(SaveAsAction.appliesTo(actionContext)).toBe(false); + }); + + it("returns to browse after save", function () { + spyOn(action, "save"); + action.save.andReturn(mockPromise(mockDomainObject)); + action.perform(); + expect(mockActionCapability.perform).toHaveBeenCalledWith( + "navigate" + ); + }); + + it("prompts the user for object details", function () { + action.perform(); + expect(mockDialogService.getUserInput).toHaveBeenCalled(); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/controls/_buttons.scss b/platform/commonUI/general/res/sass/controls/_buttons.scss index ddcca833ba..7b585f8eec 100644 --- a/platform/commonUI/general/res/sass/controls/_buttons.scss +++ b/platform/commonUI/general/res/sass/controls/_buttons.scss @@ -76,6 +76,11 @@ $pad: $interiorMargin * $baseRatio; font-family: symbolsfont; margin-right: $interiorMarginSm; } + &.t-save-as:before { + content:'\e612'; + font-family: symbolsfont; + margin-right: $interiorMarginSm; + } &.t-cancel { .title-label { display: none; } &:before { From 6d58f23c0cd0ce77c1b309ab9cabeb9860e90c4d Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 13 Apr 2016 13:23:33 -0700 Subject: [PATCH 15/38] [Style] Switch to prototype Switch TimeRangeController to prototype style, and update tests and template to utilize new style. --- .../templates/controls/time-controller.html | 35 +- .../src/controllers/TimeRangeController.js | 490 +++++++++--------- .../controllers/TimeRangeControllerSpec.js | 28 +- 3 files changed, 287 insertions(+), 266 deletions(-) diff --git a/platform/commonUI/general/res/templates/controls/time-controller.html b/platform/commonUI/general/res/templates/controls/time-controller.html index bd07659f8e..281447e251 100644 --- a/platform/commonUI/general/res/templates/controls/time-controller.html +++ b/platform/commonUI/general/res/templates/controls/time-controller.html @@ -19,15 +19,18 @@ this source code distribution or the Licensing information page available at runtime from the About dialog for additional information. --> -
+
+ ng-submit="trCtrl.updateBoundsFromForm()"> C @@ -37,9 +40,12 @@   @@ -53,22 +59,25 @@
{{startInnerText}}
{{endInnerText}}
+ mct-drag-down="trCtrl.startMiddleDrag()" + mct-drag="trCtrl.middleDrag(delta[0])" + ng-style="{ + left: startInnerPct, + right: endInnerPct + }">
diff --git a/platform/commonUI/general/src/controllers/TimeRangeController.js b/platform/commonUI/general/src/controllers/TimeRangeController.js index f0e3da46d9..3ee05e1faf 100644 --- a/platform/commonUI/general/src/controllers/TimeRangeController.js +++ b/platform/commonUI/general/src/controllers/TimeRangeController.js @@ -19,247 +19,259 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -/*global define,Promise*/ +/*global define*/ -define( - ['moment'], - function (moment) { - "use strict"; +define([ - var TICK_SPACING_PX = 150; +], function () { + "use strict"; + var TICK_SPACING_PX = 150; - /** - * Controller used by the `time-controller` template. - * @memberof platform/commonUI/general - * @constructor - * @param $scope the Angular scope for this controller - * @param {FormatService} formatService the service to user to format - * domain values - * @param {string} defaultFormat the format to request when no - * format has been otherwise specified - * @param {Function} now a function to return current system time - */ - function TimeRangeController($scope, formatService, defaultFormat, now) { - var tickCount = 2, - innerMinimumSpan = 1000, // 1 second - outerMinimumSpan = 1000, // 1 second - initialDragValue, - formatter = formatService.getFormat(defaultFormat); - - function formatTimestamp(ts) { - return formatter.format(ts); - } - - // From 0.0-1.0 to "0%"-"100%" - function toPercent(p) { - return (100 * p) + "%"; - } - - function updateTicks() { - var i, p, ts, start, end, span; - end = $scope.ngModel.outer.end; - start = $scope.ngModel.outer.start; - span = end - start; - $scope.ticks = []; - for (i = 0; i < tickCount; i += 1) { - p = i / (tickCount - 1); - ts = p * span + start; - $scope.ticks.push(formatTimestamp(ts)); - } - } - - function updateSpanWidth(w) { - tickCount = Math.max(Math.floor(w / TICK_SPACING_PX), 2); - updateTicks(); - } - - function updateViewForInnerSpanFromModel(ngModel) { - var span = ngModel.outer.end - ngModel.outer.start; - - // Expose readable dates for the knobs - $scope.startInnerText = formatTimestamp(ngModel.inner.start); - $scope.endInnerText = formatTimestamp(ngModel.inner.end); - - // And positions for the knobs - $scope.startInnerPct = - toPercent((ngModel.inner.start - ngModel.outer.start) / span); - $scope.endInnerPct = - toPercent((ngModel.outer.end - ngModel.inner.end) / span); - } - - function defaultBounds() { - var t = now(); - return { - start: t - 24 * 3600 * 1000, // One day - end: t - }; - } - - function copyBounds(bounds) { - return { start: bounds.start, end: bounds.end }; - } - - function updateViewFromModel(ngModel) { - ngModel = ngModel || {}; - ngModel.outer = ngModel.outer || defaultBounds(); - ngModel.inner = ngModel.inner || copyBounds(ngModel.outer); - - // Stick it back is scope (in case we just set defaults) - $scope.ngModel = ngModel; - - updateViewForInnerSpanFromModel(ngModel); - updateTicks(); - } - - function startLeftDrag() { - initialDragValue = $scope.ngModel.inner.start; - } - - function startRightDrag() { - initialDragValue = $scope.ngModel.inner.end; - } - - function startMiddleDrag() { - initialDragValue = { - start: $scope.ngModel.inner.start, - end: $scope.ngModel.inner.end - }; - } - - function toMillis(pixels) { - var span = - $scope.ngModel.outer.end - $scope.ngModel.outer.start; - return (pixels / $scope.spanWidth) * span; - } - - function clamp(value, low, high) { - return Math.max(low, Math.min(high, value)); - } - - function leftDrag(pixels) { - var delta = toMillis(pixels); - $scope.ngModel.inner.start = clamp( - initialDragValue + delta, - $scope.ngModel.outer.start, - $scope.ngModel.inner.end - innerMinimumSpan - ); - updateViewFromModel($scope.ngModel); - } - - function rightDrag(pixels) { - var delta = toMillis(pixels); - $scope.ngModel.inner.end = clamp( - initialDragValue + delta, - $scope.ngModel.inner.start + innerMinimumSpan, - $scope.ngModel.outer.end - ); - updateViewFromModel($scope.ngModel); - } - - function middleDrag(pixels) { - var delta = toMillis(pixels), - edge = delta < 0 ? 'start' : 'end', - opposite = delta < 0 ? 'end' : 'start'; - - // Adjust the position of the edge in the direction of drag - $scope.ngModel.inner[edge] = clamp( - initialDragValue[edge] + delta, - $scope.ngModel.outer.start, - $scope.ngModel.outer.end - ); - // Adjust opposite knob to maintain span - $scope.ngModel.inner[opposite] = $scope.ngModel.inner[edge] + - initialDragValue[opposite] - initialDragValue[edge]; - - updateViewFromModel($scope.ngModel); - } - - function updateFormModel() { - $scope.formModel = { - start: (($scope.ngModel || {}).outer || {}).start, - end: (($scope.ngModel || {}).outer || {}).end - }; - } - - function updateOuterStart(t) { - var ngModel = $scope.ngModel; - - ngModel.inner.start = - Math.max(ngModel.outer.start, ngModel.inner.start); - ngModel.inner.end = Math.max( - ngModel.inner.start + innerMinimumSpan, - ngModel.inner.end - ); - - updateFormModel(); - updateViewForInnerSpanFromModel(ngModel); - updateTicks(); - } - - function updateOuterEnd(t) { - var ngModel = $scope.ngModel; - - ngModel.inner.end = - Math.min(ngModel.outer.end, ngModel.inner.end); - ngModel.inner.start = Math.min( - ngModel.inner.end - innerMinimumSpan, - ngModel.inner.start - ); - - updateFormModel(); - updateViewForInnerSpanFromModel(ngModel); - updateTicks(); - } - - function updateFormat(key) { - formatter = formatService.getFormat(key || defaultFormat); - updateViewForInnerSpanFromModel($scope.ngModel); - updateTicks(); - } - - function updateBoundsFromForm() { - var start = $scope.formModel.start, - end = $scope.formModel.end; - if (end >= start + outerMinimumSpan) { - $scope.ngModel = $scope.ngModel || {}; - $scope.ngModel.outer = { start: start, end: end }; - } - } - - function validateStart(startValue) { - return startValue <= $scope.formModel.end - outerMinimumSpan; - } - - function validateEnd(endValue) { - return endValue >= $scope.formModel.start + outerMinimumSpan; - } - - $scope.startLeftDrag = startLeftDrag; - $scope.startRightDrag = startRightDrag; - $scope.startMiddleDrag = startMiddleDrag; - $scope.leftDrag = leftDrag; - $scope.rightDrag = rightDrag; - $scope.middleDrag = middleDrag; - - $scope.updateBoundsFromForm = updateBoundsFromForm; - - $scope.validateStart = validateStart; - $scope.validateEnd = validateEnd; - - $scope.ticks = []; - - // Initialize scope to defaults - updateViewFromModel($scope.ngModel); - updateFormModel(); - - $scope.$watchCollection("ngModel", updateViewFromModel); - $scope.$watch("spanWidth", updateSpanWidth); - $scope.$watch("ngModel.outer.start", updateOuterStart); - $scope.$watch("ngModel.outer.end", updateOuterEnd); - $scope.$watch("parameters.format", updateFormat); - } - - return TimeRangeController; + /* format number as percent; 0.0-1.0 to "0%"-"100%" */ + function toPercent(p) { + return (100 * p) + "%"; } -); + + function clamp(value, low, high) { + return Math.max(low, Math.min(high, value)); + } + + function copyBounds(bounds) { + return { + start: bounds.start, + end: bounds.end + }; + } + + /** + * Controller used by the `time-controller` template. + * @memberof platform/commonUI/general + * @constructor + * @param $scope the Angular scope for this controller + * @param {FormatService} formatService the service to user to format + * domain values + * @param {string} defaultFormat the format to request when no + * format has been otherwise specified + * @param {Function} now a function to return current system time + */ + function TimeRangeController($scope, formatService, defaultFormat, now) { + this.$scope = $scope; + this.formatService = formatService; + this.defaultFormat = defaultFormat; + this.now = now; + + this.tickCount = 2; + this.innerMinimumSpan = 1000; // 1 second + this.outerMinimumSpan = 1000; // 1 second + this.initialDragValue = undefined; + this.formatter = formatService.getFormat(defaultFormat); + + this.$scope.ticks = []; + + this.updateViewFromModel(this.$scope.ngModel); + this.updateFormModel(); + + [ + 'updateViewFromModel', + 'updateSpanWidth', + 'updateOuterStart', + 'updateOuterEnd', + 'updateFormat', + 'validateStart', + 'validateEnd' + ].forEach(function (boundFn) { + this[boundFn] = this[boundFn].bind(this); + }, this); + + this.$scope.$watchCollection("ngModel", this.updateViewFromModel); + this.$scope.$watch("spanWidth", this.updateSpanWidth); + this.$scope.$watch("ngModel.outer.start", this.updateOuterStart); + this.$scope.$watch("ngModel.outer.end", this.updateOuterEnd); + this.$scope.$watch("parameters.format", this.updateFormat); + } + + TimeRangeController.prototype.formatTimestamp = function (ts) { + return this.formatter.format(ts); + }; + + TimeRangeController.prototype.updateTicks = function () { + var i, p, ts, start, end, span; + end = this.$scope.ngModel.outer.end; + start = this.$scope.ngModel.outer.start; + span = end - start; + this.$scope.ticks = []; + for (i = 0; i < this.tickCount; i += 1) { + p = i / (this.tickCount - 1); + ts = p * span + start; + this.$scope.ticks.push(this.formatTimestamp(ts)); + } + }; + + TimeRangeController.prototype.updateSpanWidth = function (w) { + this.tickCount = Math.max(Math.floor(w / TICK_SPACING_PX), 2); + this.updateTicks(); + }; + + TimeRangeController.prototype.updateViewForInnerSpanFromModel = function ( + ngModel + ) { + var span = ngModel.outer.end - ngModel.outer.start; + + // Expose readable dates for the knobs + this.$scope.startInnerText = this.formatTimestamp(ngModel.inner.start); + this.$scope.endInnerText = this.formatTimestamp(ngModel.inner.end); + + // And positions for the knobs + this.$scope.startInnerPct = + toPercent((ngModel.inner.start - ngModel.outer.start) / span); + this.$scope.endInnerPct = + toPercent((ngModel.outer.end - ngModel.inner.end) / span); + }; + + TimeRangeController.prototype.defaultBounds = function () { + var t = this.now(); + return { + start: t - 24 * 3600 * 1000, // One day + end: t + }; + }; + + + TimeRangeController.prototype.updateViewFromModel = function (ngModel) { + ngModel = ngModel || {}; + ngModel.outer = ngModel.outer || this.defaultBounds(); + ngModel.inner = ngModel.inner || copyBounds(ngModel.outer); + + // Stick it back is scope (in case we just set defaults) + this.$scope.ngModel = ngModel; + + this.updateViewForInnerSpanFromModel(ngModel); + this.updateTicks(); + }; + + TimeRangeController.prototype.startLeftDrag = function () { + this.initialDragValue = this.$scope.ngModel.inner.start; + }; + + TimeRangeController.prototype.startRightDrag = function () { + this.initialDragValue = this.$scope.ngModel.inner.end; + }; + + TimeRangeController.prototype.startMiddleDrag = function () { + this.initialDragValue = { + start: this.$scope.ngModel.inner.start, + end: this.$scope.ngModel.inner.end + }; + }; + + TimeRangeController.prototype.toMillis = function (pixels) { + var span = + this.$scope.ngModel.outer.end - this.$scope.ngModel.outer.start; + return (pixels / this.$scope.spanWidth) * span; + }; + + TimeRangeController.prototype.leftDrag = function (pixels) { + var delta = this.toMillis(pixels); + this.$scope.ngModel.inner.start = clamp( + this.initialDragValue + delta, + this.$scope.ngModel.outer.start, + this.$scope.ngModel.inner.end - this.innerMinimumSpan + ); + this.updateViewFromModel(this.$scope.ngModel); + }; + + TimeRangeController.prototype.rightDrag = function (pixels) { + var delta = this.toMillis(pixels); + this.$scope.ngModel.inner.end = clamp( + this.initialDragValue + delta, + this.$scope.ngModel.inner.start + this.innerMinimumSpan, + this.$scope.ngModel.outer.end + ); + this.updateViewFromModel(this.$scope.ngModel); + }; + + TimeRangeController.prototype.middleDrag = function (pixels) { + var delta = this.toMillis(pixels), + edge = delta < 0 ? 'start' : 'end', + opposite = delta < 0 ? 'end' : 'start'; + + // Adjust the position of the edge in the direction of drag + this.$scope.ngModel.inner[edge] = clamp( + this.initialDragValue[edge] + delta, + this.$scope.ngModel.outer.start, + this.$scope.ngModel.outer.end + ); + // Adjust opposite knob to maintain span + this.$scope.ngModel.inner[opposite] = + this.$scope.ngModel.inner[edge] + + this.initialDragValue[opposite] - + this.initialDragValue[edge]; + + this.updateViewFromModel(this.$scope.ngModel); + }; + + TimeRangeController.prototype.updateFormModel = function () { + this.$scope.formModel = { + start: ((this.$scope.ngModel || {}).outer || {}).start, + end: ((this.$scope.ngModel || {}).outer || {}).end + }; + }; + + TimeRangeController.prototype.updateOuterStart = function () { + var ngModel = this.$scope.ngModel; + + ngModel.inner.start = + Math.max(ngModel.outer.start, ngModel.inner.start); + ngModel.inner.end = Math.max( + ngModel.inner.start + this.innerMinimumSpan, + ngModel.inner.end + ); + + this.updateFormModel(); + this.updateViewForInnerSpanFromModel(ngModel); + this.updateTicks(); + }; + + TimeRangeController.prototype.updateOuterEnd = function () { + var ngModel = this.$scope.ngModel; + + ngModel.inner.end = + Math.min(ngModel.outer.end, ngModel.inner.end); + ngModel.inner.start = Math.min( + ngModel.inner.end - this.innerMinimumSpan, + ngModel.inner.start + ); + + this.updateFormModel(); + this.updateViewForInnerSpanFromModel(ngModel); + this.updateTicks(); + }; + + TimeRangeController.prototype.updateFormat = function (key) { + this.formatter = this.formatService.getFormat(key || this.defaultFormat); + this.updateViewForInnerSpanFromModel(this.$scope.ngModel); + this.updateTicks(); + }; + + TimeRangeController.prototype.updateBoundsFromForm = function () { + var start = this.$scope.formModel.start, + end = this.$scope.formModel.end; + if (end >= start + this.outerMinimumSpan) { + this.$scope.ngModel = this.$scope.ngModel || {}; + this.$scope.ngModel.outer = { start: start, end: end }; + } + }; + + TimeRangeController.prototype.validateStart = function (startValue) { + return startValue <= + this.$scope.formModel.end - this.outerMinimumSpan; + }; + + TimeRangeController.prototype.validateEnd = function (endValue) { + return endValue >= + this.$scope.formModel.start + this.outerMinimumSpan; + }; + + return TimeRangeController; +}); diff --git a/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js index efff87651f..086947950a 100644 --- a/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js +++ b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js @@ -94,18 +94,18 @@ define( it("exposes start time validator", function () { var testValue = 42000000; mockScope.formModel = { end: testValue }; - expect(mockScope.validateStart(testValue + 1)) + expect(controller.validateStart(testValue + 1)) .toBe(false); - expect(mockScope.validateStart(testValue - 60 * 60 * 1000 - 1)) + expect(controller.validateStart(testValue - 60 * 60 * 1000 - 1)) .toBe(true); }); it("exposes end time validator", function () { var testValue = 42000000; mockScope.formModel = { start: testValue }; - expect(mockScope.validateEnd(testValue - 1)) + expect(controller.validateEnd(testValue - 1)) .toBe(false); - expect(mockScope.validateEnd(testValue + 60 * 60 * 1000 + 1)) + expect(controller.validateEnd(testValue + 60 * 60 * 1000 + 1)) .toBe(true); }); @@ -134,7 +134,7 @@ define( }); it("updates model bounds on request", function () { - mockScope.updateBoundsFromForm(); + controller.updateBoundsFromForm(); expect(mockScope.ngModel.outer.start) .toEqual(mockScope.formModel.start); expect(mockScope.ngModel.outer.end) @@ -160,27 +160,27 @@ define( }); it("updates the start time for left drags", function () { - mockScope.startLeftDrag(); - mockScope.leftDrag(250); + controller.startLeftDrag(); + controller.leftDrag(250); expect(mockScope.ngModel.inner.start) .toEqual(DAY * 1000 + HOUR * 9); }); it("updates the end time for right drags", function () { - mockScope.startRightDrag(); - mockScope.rightDrag(-250); + controller.startRightDrag(); + controller.rightDrag(-250); expect(mockScope.ngModel.inner.end) .toEqual(DAY * 1000 + HOUR * 15); }); it("updates both start and end for middle drags", function () { - mockScope.startMiddleDrag(); - mockScope.middleDrag(-125); + controller.startMiddleDrag(); + controller.middleDrag(-125); expect(mockScope.ngModel.inner).toEqual({ start: DAY * 1000, end: DAY * 1000 + HOUR * 18 }); - mockScope.middleDrag(250); + controller.middleDrag(250); expect(mockScope.ngModel.inner).toEqual({ start: DAY * 1000 + HOUR * 6, end: DAY * 1001 @@ -188,8 +188,8 @@ define( }); it("enforces a minimum inner span", function () { - mockScope.startRightDrag(); - mockScope.rightDrag(-9999999); + controller.startRightDrag(); + controller.rightDrag(-9999999); expect(mockScope.ngModel.inner.end) .toBeGreaterThan(mockScope.ngModel.inner.start); }); From 69c059c943fab633c429ed18f0460c25bbdd5ea9 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 13 Apr 2016 13:49:23 -0700 Subject: [PATCH 16/38] [Conductor] Update inner bound on blur The time conductor updates the inner and outer bounds when the input is blurred, which results in the query updating without dragging. Also allows time conductor to be utilized on mobile devices by entering dates directly. https://github.com/nasa/openmct/issues/318 User request: https://github.jpl.nasa.gov/MissionControl/vista/issues/175 --- .../src/controllers/TimeRangeController.js | 42 ++++++++-- .../controllers/TimeRangeControllerSpec.js | 76 +++++++++++++++++-- 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/platform/commonUI/general/src/controllers/TimeRangeController.js b/platform/commonUI/general/src/controllers/TimeRangeController.js index 3ee05e1faf..eca72ce7ab 100644 --- a/platform/commonUI/general/src/controllers/TimeRangeController.js +++ b/platform/commonUI/general/src/controllers/TimeRangeController.js @@ -66,6 +66,8 @@ define([ this.outerMinimumSpan = 1000; // 1 second this.initialDragValue = undefined; this.formatter = formatService.getFormat(defaultFormat); + this.formStartChanged = false; + this.formEndChanged = false; this.$scope.ticks = []; @@ -79,7 +81,9 @@ define([ 'updateOuterEnd', 'updateFormat', 'validateStart', - 'validateEnd' + 'validateEnd', + 'onFormStartChange', + 'onFormEndChange' ].forEach(function (boundFn) { this[boundFn] = this[boundFn].bind(this); }, this); @@ -89,6 +93,8 @@ define([ this.$scope.$watch("ngModel.outer.start", this.updateOuterStart); this.$scope.$watch("ngModel.outer.end", this.updateOuterEnd); this.$scope.$watch("parameters.format", this.updateFormat); + this.$scope.$watch("formModel.start", this.onFormStartChange); + this.$scope.$watch("formModel.end", this.onFormEndChange); } TimeRangeController.prototype.formatTimestamp = function (ts) { @@ -255,11 +261,35 @@ define([ }; TimeRangeController.prototype.updateBoundsFromForm = function () { - var start = this.$scope.formModel.start, - end = this.$scope.formModel.end; - if (end >= start + this.outerMinimumSpan) { - this.$scope.ngModel = this.$scope.ngModel || {}; - this.$scope.ngModel.outer = { start: start, end: end }; + if (this.formStartChanged) { + this.$scope.ngModel.outer.start = + this.$scope.ngModel.inner.start = + this.$scope.formModel.start; + this.formStartChanged = false; + } + if (this.formEndChanged) { + this.$scope.ngModel.outer.end = + this.$scope.ngModel.inner.end = + this.$scope.formModel.end; + this.formEndChanged = false; + } + }; + + TimeRangeController.prototype.onFormStartChange = function ( + newValue, + oldValue + ) { + if (!this.formStartChanged && newValue !== oldValue) { + this.formStartChanged = true; + } + }; + + TimeRangeController.prototype.onFormEndChange = function ( + newValue, + oldValue + ) { + if (!this.formEndChanged && newValue !== oldValue) { + this.formEndChanged = true; } }; diff --git a/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js index 086947950a..20ad0413b6 100644 --- a/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js +++ b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js @@ -119,25 +119,87 @@ define( start: DAY * 10000, end: DAY * 11000 }; - // These watches may not exist, but Angular would fire - // them if they did. + }); + + it('updates all changed bounds when requested', function () { fireWatchCollection("formModel", mockScope.formModel); fireWatch("formModel.start", mockScope.formModel.start); fireWatch("formModel.end", mockScope.formModel.end); - }); - it("does not immediately make changes to the model", function () { expect(mockScope.ngModel.outer.start) .not.toEqual(mockScope.formModel.start); + expect(mockScope.ngModel.inner.start) + .not.toEqual(mockScope.formModel.start); + expect(mockScope.ngModel.outer.end) .not.toEqual(mockScope.formModel.end); + expect(mockScope.ngModel.inner.end) + .not.toEqual(mockScope.formModel.end); + + controller.updateBoundsFromForm(); + + expect(mockScope.ngModel.outer.start) + .toEqual(mockScope.formModel.start); + expect(mockScope.ngModel.inner.start) + .toEqual(mockScope.formModel.start); + + expect(mockScope.ngModel.outer.end) + .toEqual(mockScope.formModel.end); + expect(mockScope.ngModel.inner.end) + .toEqual(mockScope.formModel.end); + }); + + it('updates changed start bound when requested', function () { + fireWatchCollection("formModel", mockScope.formModel); + fireWatch("formModel.start", mockScope.formModel.start); + + expect(mockScope.ngModel.outer.start) + .not.toEqual(mockScope.formModel.start); + expect(mockScope.ngModel.inner.start) + .not.toEqual(mockScope.formModel.start); + + expect(mockScope.ngModel.outer.end) + .not.toEqual(mockScope.formModel.end); + expect(mockScope.ngModel.inner.end) + .not.toEqual(mockScope.formModel.end); + + controller.updateBoundsFromForm(); + + expect(mockScope.ngModel.outer.start) + .toEqual(mockScope.formModel.start); + expect(mockScope.ngModel.inner.start) + .toEqual(mockScope.formModel.start); + + expect(mockScope.ngModel.outer.end) + .not.toEqual(mockScope.formModel.end); + expect(mockScope.ngModel.inner.end) + .not.toEqual(mockScope.formModel.end); }); - it("updates model bounds on request", function () { - controller.updateBoundsFromForm(); + it('updates changed end bound when requested', function () { + fireWatchCollection("formModel", mockScope.formModel); + fireWatch("formModel.end", mockScope.formModel.end); + expect(mockScope.ngModel.outer.start) - .toEqual(mockScope.formModel.start); + .not.toEqual(mockScope.formModel.start); + expect(mockScope.ngModel.inner.start) + .not.toEqual(mockScope.formModel.start); + expect(mockScope.ngModel.outer.end) + .not.toEqual(mockScope.formModel.end); + expect(mockScope.ngModel.inner.end) + .not.toEqual(mockScope.formModel.end); + + controller.updateBoundsFromForm(); + + expect(mockScope.ngModel.outer.start) + .not.toEqual(mockScope.formModel.start); + expect(mockScope.ngModel.inner.start) + .not.toEqual(mockScope.formModel.start); + + expect(mockScope.ngModel.outer.end) + .toEqual(mockScope.formModel.end); + expect(mockScope.ngModel.inner.end) .toEqual(mockScope.formModel.end); }); }); From 7a7877d7c4b7907b4f0a782b26ed613ea12e2121 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 13 Apr 2016 16:17:17 -0700 Subject: [PATCH 17/38] [Conductor] Add basic style for phone Add styling for time conductor on mobile that removes slider and rearranges inputs to utilize space more effectively. https://github.com/nasa/openmct/issues/318 --- .../res/sass/controls/_time-controller.scss | 72 ++++++++++++++++++- .../res/templates/time-conductor.html | 2 +- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/platform/commonUI/general/res/sass/controls/_time-controller.scss b/platform/commonUI/general/res/sass/controls/_time-controller.scss index 49d996102d..fb37df77cf 100644 --- a/platform/commonUI/general/res/sass/controls/_time-controller.scss +++ b/platform/commonUI/general/res/sass/controls/_time-controller.scss @@ -6,6 +6,10 @@ } } +// Question for Charles: I believe this mct-include is unnecessary here. It +// adds more specificity to the selector and makes the later override for +// mobile styling also require more specificity. Without knowing whether or +// not there is a specific reason this specifier is used, I won't remove it. mct-include.l-time-controller { $minW: 500px; $knobHOffset: 0px; @@ -185,6 +189,13 @@ mct-include.l-time-controller { } } } + + .l-time-domain-selector { + position: absolute; + right: 0px; + bottom: 46px; + } + } //.slot.range-holder { @@ -196,4 +207,63 @@ mct-include.l-time-controller { border-radius: $controlCr; background-color: $colorInputBg; padding: 1px 1px 0 $interiorMargin; -} \ No newline at end of file +} + +@include phoneandtablet { + mct-include.l-time-controller, mct-include.l-time-range-inputs-holder { + min-width: 0px; + } + + mct-include.l-time-controller { + height: 48px; + + .l-time-range-inputs-holder { + bottom: 24px; + } + + .l-time-domain-selector { + width: 33%; + bottom: -9px; + select { + height: 25px; + margin-bottom: 0px; + } + } + + .l-time-range-slider-holder, .l-time-range-ticks-holder { + display: none; + } + + .time-range-start, .time-range-end, { + width: 100%; + } + + .l-time-range-inputs-holder { + .l-time-range-input { + display: block; + margin-bottom: 5px; + .s-btn { + width: 66%; + padding-right: 18px; + white-space: nowrap; + input { + width: 100%; + } + } + } + .l-time-range-inputs-elem { + display: none; + &.lbl { + display: block; + height: 25px; + width: 33%; + right: 0px; + margin: 0; + top: 5px; + line-height: 25px; + position: absolute; + } + } + } + } +} diff --git a/platform/features/conductor/res/templates/time-conductor.html b/platform/features/conductor/res/templates/time-conductor.html index 16cc6296c8..1a2392b1e3 100644 --- a/platform/features/conductor/res/templates/time-conductor.html +++ b/platform/features/conductor/res/templates/time-conductor.html @@ -6,6 +6,6 @@ ng-model='ngModel' field="'domain'" options="ngModel.options" - style="position: absolute; right: 0px; bottom: 46px;" + class="l-time-domain-selector" > From 22a5122ab71d69ceb124aba3e7ed0a3f62d0d055 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 13 Apr 2016 17:17:52 -0700 Subject: [PATCH 18/38] [Conductor] Style for Phone and Tablet Specify styles for time conductor on phone an tablet to hide the slider and utilize space better. https://github.com/nasa/openmct/issues/318 --- .../res/sass/controls/_time-controller.scss | 147 ++++++++++++++++-- 1 file changed, 130 insertions(+), 17 deletions(-) diff --git a/platform/commonUI/general/res/sass/controls/_time-controller.scss b/platform/commonUI/general/res/sass/controls/_time-controller.scss index fb37df77cf..ec7aa33cb3 100644 --- a/platform/commonUI/general/res/sass/controls/_time-controller.scss +++ b/platform/commonUI/general/res/sass/controls/_time-controller.scss @@ -6,11 +6,11 @@ } } -// Question for Charles: I believe this mct-include is unnecessary here. It +// Question for Charles: I believe this is unnecessary here. It // adds more specificity to the selector and makes the later override for // mobile styling also require more specificity. Without knowing whether or // not there is a specific reason this specifier is used, I won't remove it. -mct-include.l-time-controller { +.l-time-controller { $minW: 500px; $knobHOffset: 0px; $knobM: ($sliderKnobW + $knobHOffset) * -1; @@ -210,20 +210,13 @@ mct-include.l-time-controller { } @include phoneandtablet { - mct-include.l-time-controller, mct-include.l-time-range-inputs-holder { + .l-time-controller, .l-time-range-inputs-holder { min-width: 0px; } - mct-include.l-time-controller { - height: 48px; - - .l-time-range-inputs-holder { - bottom: 24px; - } + .l-time-controller { .l-time-domain-selector { - width: 33%; - bottom: -9px; select { height: 25px; margin-bottom: 0px; @@ -241,9 +234,7 @@ mct-include.l-time-controller { .l-time-range-inputs-holder { .l-time-range-input { display: block; - margin-bottom: 5px; .s-btn { - width: 66%; padding-right: 18px; white-space: nowrap; input { @@ -252,14 +243,44 @@ mct-include.l-time-controller { } } .l-time-range-inputs-elem { - display: none; + + } + } + } +} + +@include phone { + .l-time-controller { + height: 48px; + + .l-time-range-inputs-holder { + bottom: 24px; + } + + .l-time-domain-selector { + width: 33%; + bottom: -9px; + } + + .l-time-range-inputs-holder { + .l-time-range-input { + margin-bottom: 5px; + .s-btn { + width: 66%; + } + } + .l-time-range-inputs-elem { + &.ui-symbol { + display: none; + } + &.lbl { - display: block; - height: 25px; width: 33%; right: 0px; - margin: 0; top: 5px; + display: block; + height: 25px; + margin: 0; line-height: 25px; position: absolute; } @@ -267,3 +288,95 @@ mct-include.l-time-controller { } } } + + +@include tablet { + .l-time-controller { + height: 17px; + + .l-time-range-inputs-holder { + bottom: -7px; // 24 to -> -7 = -31px + left: -5px; + } + + .l-time-domain-selector { + width: 23%; + right: -4px; + bottom: -10px; + } + + .l-time-range-inputs-holder { + .l-time-range-input { + float: left; + .s-btn { + width: 100%; + padding-left: 4px; + } + } + } + } +} + +@include tabletLandscape { + .l-time-controller { + height: 17px; + + .l-time-range-inputs-holder { + bottom: -7px; // 24 to -> -7 = -31px + } + + .l-time-domain-selector { + width: 23%; + right: auto; + bottom: -10px; + left: 391px; + } + + .l-time-range-inputs-holder { + .l-time-range-inputs-elem { + &.ui-symbol, &.lbl { + display: block; + float: left; + line-height: 25px; + } + } + } + } + + .pane-tree-hidden .l-time-controller { + .l-time-domain-selector { + left: 667px; + } + .l-time-range-inputs-holder { + padding-left: 277px; + } + } +} +@include tabletPortrait { + .l-time-controller { + height: 17px; + + .l-time-range-inputs-holder { + bottom: -7px; + left: -5px; + } + + .l-time-domain-selector { + width: 23%; + right: -4px; + bottom: -10px; + } + + .l-time-range-inputs-holder { + .l-time-range-input { + width: 38%; + float: left; + } + .l-time-range-inputs-elem { + &.ui-symbol, &.lbl { + display: none; + } + } + } + } +} From b682cf83407f6ab53bc3dd1bfaac3349a2de80d2 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 13 Apr 2016 17:41:24 -0700 Subject: [PATCH 19/38] [Style] Remove outdated comments --- .../res/sass/controls/_time-controller.scss | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/platform/commonUI/general/res/sass/controls/_time-controller.scss b/platform/commonUI/general/res/sass/controls/_time-controller.scss index ec7aa33cb3..f6201d60cf 100644 --- a/platform/commonUI/general/res/sass/controls/_time-controller.scss +++ b/platform/commonUI/general/res/sass/controls/_time-controller.scss @@ -1,58 +1,42 @@ @mixin toiLineHovEffects() { - //@include pulse(.25s); &:before, &:after { background-color: $timeControllerToiLineColorHov; } } -// Question for Charles: I believe this is unnecessary here. It -// adds more specificity to the selector and makes the later override for -// mobile styling also require more specificity. Without knowing whether or -// not there is a specific reason this specifier is used, I won't remove it. .l-time-controller { $minW: 500px; $knobHOffset: 0px; $knobM: ($sliderKnobW + $knobHOffset) * -1; $rangeValPad: $interiorMargin; $rangeValOffset: $sliderKnobW; - //$knobCr: $sliderKnobW; $timeRangeSliderLROffset: 130px + $sliderKnobW + $rangeValOffset; $r1H: nth($ueTimeControlH,1); $r2H: nth($ueTimeControlH,2); $r3H: nth($ueTimeControlH,3); - //@include absPosDefault(); - //@include test(); display: block; - //top: auto; height: $r1H + $r2H + $r3H + ($interiorMargin * 2); min-width: $minW; font-size: 0.8rem; - .l-time-range-inputs-holder, - .l-time-range-slider { - //font-size: 0.8em; - } .l-time-range-inputs-holder, .l-time-range-slider-holder, .l-time-range-ticks-holder { - //@include test(); @include absPosDefault(0, visible); box-sizing: border-box; top: auto; } .l-time-range-slider, .l-time-range-ticks { - //@include test(red, 0.1); @include absPosDefault(0, visible); left: $timeRangeSliderLROffset; right: $timeRangeSliderLROffset; } .l-time-range-inputs-holder { - //@include test(red); height: $r1H; bottom: $r2H + $r3H + ($interiorMarginSm * 2); padding-top: $interiorMargin; border-top: 1px solid $colorInteriorBorder; @@ -74,7 +58,6 @@ } .l-time-range-slider-holder { - //@include test(green); height: $r2H; bottom: $r3H + ($interiorMarginSm * 1); .range-holder { box-shadow: none; @@ -86,7 +69,6 @@ $myW: 8px; @include transform(translateX(50%)); position: absolute; - //@include test(); top: 0; right: 0; bottom: 0px; left: auto; width: $myW; height: auto; @@ -101,7 +83,6 @@ // Vert line top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1; width: 2px; - //top: 0; right: 3px; bottom: 0; left: 3px; } &:after { // Circle element @@ -118,7 +99,6 @@ } } &:not(:active) { - //@include test(#ff00cc); .knob, .range { @include transition-property(left, right); @@ -159,7 +139,6 @@ .knob { z-index: 2; .range-value { - //@include test($sliderColorRange); @include trans-prop-nice-fade(.25s); padding: 0 $rangeValOffset; position: absolute; @@ -171,7 +150,6 @@ color: $sliderColorKnobHov; } &.knob-l { - //border-bottom-left-radius: $knobCr; // MOVED TO _CONTROLS.SCSS margin-left: $knobM; .range-value { text-align: right; @@ -179,7 +157,6 @@ } } &.knob-r { - //border-bottom-right-radius: $knobCr; margin-right: $knobM; .range-value { left: $rangeValOffset; @@ -198,12 +175,7 @@ } -//.slot.range-holder { -// background-color: $sliderColorRangeHolder; -//} - .s-time-range-val { - //@include test(); border-radius: $controlCr; background-color: $colorInputBg; padding: 1px 1px 0 $interiorMargin; @@ -295,7 +267,7 @@ height: 17px; .l-time-range-inputs-holder { - bottom: -7px; // 24 to -> -7 = -31px + bottom: -7px; left: -5px; } @@ -322,7 +294,7 @@ height: 17px; .l-time-range-inputs-holder { - bottom: -7px; // 24 to -> -7 = -31px + bottom: -7px; } .l-time-domain-selector { From ffd5faf9a21e0c7f1bca3b3c394b522555142eaf Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 14 Apr 2016 10:15:40 -0700 Subject: [PATCH 20/38] [Persistence] Remove obsolete caching decorator #831 --- platform/persistence/cache/README.md | 2 - platform/persistence/cache/bundle.js | 49 ------ .../cache/src/CachingPersistenceDecorator.js | 163 ------------------ .../test/CachingPersistenceDecoratorSpec.js | 144 ---------------- 4 files changed, 358 deletions(-) delete mode 100644 platform/persistence/cache/README.md delete mode 100644 platform/persistence/cache/bundle.js delete mode 100644 platform/persistence/cache/src/CachingPersistenceDecorator.js delete mode 100644 platform/persistence/cache/test/CachingPersistenceDecoratorSpec.js diff --git a/platform/persistence/cache/README.md b/platform/persistence/cache/README.md deleted file mode 100644 index 91c9a2cb69..0000000000 --- a/platform/persistence/cache/README.md +++ /dev/null @@ -1,2 +0,0 @@ -This bundle introduces a persistence cache to Open MCT Web to -limit the number of persistence requests issued by the platform. diff --git a/platform/persistence/cache/bundle.js b/platform/persistence/cache/bundle.js deleted file mode 100644 index 4450a72c8a..0000000000 --- a/platform/persistence/cache/bundle.js +++ /dev/null @@ -1,49 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web 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 Web 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. - *****************************************************************************/ -/*global define*/ - -define([ - "./src/CachingPersistenceDecorator", - 'legacyRegistry' -], function ( - CachingPersistenceDecorator, - legacyRegistry -) { - "use strict"; - - legacyRegistry.register("platform/persistence/cache", { - "name": "Persistence cache", - "description": "Cache to improve availability of persisted objects.", - "extensions": { - "components": [ - { - "provides": "persistenceService", - "type": "decorator", - "implementation": CachingPersistenceDecorator, - "depends": [ - "PERSISTENCE_SPACE" - ] - } - ] - } - }); -}); diff --git a/platform/persistence/cache/src/CachingPersistenceDecorator.js b/platform/persistence/cache/src/CachingPersistenceDecorator.js deleted file mode 100644 index 3053bc088a..0000000000 --- a/platform/persistence/cache/src/CachingPersistenceDecorator.js +++ /dev/null @@ -1,163 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web 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 Web 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. - *****************************************************************************/ -/*global define*/ - -/** - * This bundle decorates the persistence service to maintain a local cache - * of persisted documents. - * @namespace platform/persistence/cache - */ -define( - [], - function () { - 'use strict'; - - /** - * A caching persistence decorator maintains local copies of persistent objects - * that have been loaded, and keeps them in sync after writes. This allows - * retrievals to occur more quickly after the first load. - * - * @memberof platform/persistence/cache - * @constructor - * @param {string[]} cacheSpaces persistence space names which - * should be cached - * @param {PersistenceService} persistenceService the service which - * implements object persistence, whose inputs/outputs - * should be cached. - * @implements {PersistenceService} - */ - function CachingPersistenceDecorator(cacheSpaces, persistenceService) { - var spaces = cacheSpaces || [], // List of spaces to cache - cache = {}; // Where objects will be stored - - // Arrayify list of spaces to cache, if necessary. - spaces = Array.isArray(spaces) ? spaces : [ spaces ]; - - // Initialize caches - spaces.forEach(function (space) { - cache[space] = {}; - }); - - this.spaces = spaces; - this.cache = cache; - this.persistenceService = persistenceService; - } - - // Wrap as a thenable; used instead of $q.when because that - // will resolve on a future tick, which can cause latency - // issues (which this decorator is intended to address.) - function fastPromise(value) { - return { - then: function (callback) { - return fastPromise(callback(value)); - } - }; - } - - // Update the cached instance of an object to a new value - function replaceValue(valueHolder, newValue) { - var v = valueHolder.value; - - // If it's a JS object, we want to replace contents, so that - // everybody gets the same instance. - if (typeof v === 'object' && v !== null) { - // Only update contents if these are different instances - if (v !== newValue) { - // Clear prior contents - Object.keys(v).forEach(function (k) { - delete v[k]; - }); - // Shallow-copy contents - Object.keys(newValue).forEach(function (k) { - v[k] = newValue[k]; - }); - } - } else { - // Otherwise, just store the new value - valueHolder.value = newValue; - } - } - - // Place value in the cache for space, if there is one. - CachingPersistenceDecorator.prototype.addToCache = function (space, key, value) { - var cache = this.cache; - if (cache[space]) { - if (cache[space][key]) { - replaceValue(cache[space][key], value); - } else { - cache[space][key] = { value: value }; - } - } - }; - - // Create a function for putting value into a cache; - // useful for then-chaining. - CachingPersistenceDecorator.prototype.putCache = function (space, key) { - var self = this; - return function (value) { - self.addToCache(space, key, value); - return value; - }; - }; - - - - CachingPersistenceDecorator.prototype.listSpaces = function () { - return this.persistenceService.listSpaces(); - }; - - CachingPersistenceDecorator.prototype.listObjects = function (space) { - return this.persistenceService.listObjects(space); - }; - - CachingPersistenceDecorator.prototype.createObject = function (space, key, value) { - this.addToCache(space, key, value); - return this.persistenceService.createObject(space, key, value); - }; - - CachingPersistenceDecorator.prototype.readObject = function (space, key) { - var cache = this.cache; - return (cache[space] && cache[space][key]) ? - fastPromise(cache[space][key].value) : - this.persistenceService.readObject(space, key) - .then(this.putCache(space, key)); - }; - - CachingPersistenceDecorator.prototype.updateObject = function (space, key, value) { - var self = this; - return this.persistenceService.updateObject(space, key, value) - .then(function (result) { - self.addToCache(space, key, value); - return result; - }); - }; - - CachingPersistenceDecorator.prototype.deleteObject = function (space, key, value) { - if (this.cache[space]) { - delete this.cache[space][key]; - } - return this.persistenceService.deleteObject(space, key, value); - }; - - return CachingPersistenceDecorator; - } -); diff --git a/platform/persistence/cache/test/CachingPersistenceDecoratorSpec.js b/platform/persistence/cache/test/CachingPersistenceDecoratorSpec.js deleted file mode 100644 index c515ea0fee..0000000000 --- a/platform/persistence/cache/test/CachingPersistenceDecoratorSpec.js +++ /dev/null @@ -1,144 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web 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 Web 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. - *****************************************************************************/ -/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ - -define( - ["../src/CachingPersistenceDecorator"], - function (CachingPersistenceDecorator) { - "use strict"; - - var PERSISTENCE_METHODS = [ - "listSpaces", - "listObjects", - "createObject", - "readObject", - "updateObject", - "deleteObject" - ]; - - describe("The caching persistence decorator", function () { - var testSpace, - mockPersistence, - mockCallback, - decorator; - - function mockPromise(value) { - return { - then: function (callback) { - return mockPromise(callback(value)); - } - }; - } - - beforeEach(function () { - - - testSpace = "TEST"; - mockPersistence = jasmine.createSpyObj( - "persistenceService", - PERSISTENCE_METHODS - ); - mockCallback = jasmine.createSpy("callback"); - - PERSISTENCE_METHODS.forEach(function (m) { - mockPersistence[m].andReturn(mockPromise({ - method: m - })); - }); - - decorator = new CachingPersistenceDecorator( - testSpace, - mockPersistence - ); - }); - - it("delegates all methods", function () { - PERSISTENCE_METHODS.forEach(function (m) { - // Reset the callback - mockCallback = jasmine.createSpy("callback"); - // Invoke the method; avoid using a key that will be cached - decorator[m](testSpace, "testKey" + m, "testValue") - .then(mockCallback); - // Should have gotten that method's plain response - expect(mockCallback).toHaveBeenCalledWith({ method: m }); - }); - }); - - it("does not repeat reads of cached objects", function () { - // Perform two reads - decorator.readObject(testSpace, "someKey", "someValue") - .then(mockCallback); - decorator.readObject(testSpace, "someKey", "someValue") - .then(mockCallback); - - // Should have only delegated once - expect(mockPersistence.readObject.calls.length).toEqual(1); - - // But both promises should have resolved - expect(mockCallback.calls.length).toEqual(2); - - }); - - it("gives a single instance of cached objects", function () { - // Perform two reads - decorator.readObject(testSpace, "someKey", "someValue") - .then(mockCallback); - decorator.readObject(testSpace, "someKey", "someValue") - .then(mockCallback); - - // Results should have been pointer-identical - expect(mockCallback.calls[0].args[0]) - .toBe(mockCallback.calls[1].args[0]); - }); - - it("maintains the same cached instance between reads/writes", function () { - var testObject = { abc: "XYZ!" }; - - // Perform two reads with a write in between - decorator.readObject(testSpace, "someKey", "someValue") - .then(mockCallback); - decorator.updateObject(testSpace, "someKey", testObject); - decorator.readObject(testSpace, "someKey", "someValue") - .then(mockCallback); - - // Results should have been pointer-identical - expect(mockCallback.calls[0].args[0]) - .toBe(mockCallback.calls[1].args[0]); - - // But contents should have been equal to the written object - expect(mockCallback).toHaveBeenCalledWith(testObject); - }); - - it("is capable of reading/writing strings", function () { - // Efforts made to keep cached objects pointer-identical - // would break on strings - so make sure cache isn't - // breaking when we read/write strings. - decorator.createObject(testSpace, "someKey", "someValue"); - decorator.updateObject(testSpace, "someKey", "someOtherValue"); - decorator.readObject(testSpace, "someKey").then(mockCallback); - expect(mockCallback).toHaveBeenCalledWith("someOtherValue"); - - - }); - }); - } -); \ No newline at end of file From 8c3616da32146a21d2613bea86515cfe2921adcb Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 14 Apr 2016 16:04:15 -0700 Subject: [PATCH 21/38] Addressed code review points --- platform/commonUI/browse/src/creation/CreateAction.js | 2 +- platform/commonUI/edit/bundle.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/platform/commonUI/browse/src/creation/CreateAction.js b/platform/commonUI/browse/src/creation/CreateAction.js index dd8f227446..83d88ba709 100644 --- a/platform/commonUI/browse/src/creation/CreateAction.js +++ b/platform/commonUI/browse/src/creation/CreateAction.js @@ -103,7 +103,7 @@ define( if (countEditableViews(editableObject) > 0 && editableObject.hasCapability('composition')) { this.navigationService.setNavigation(editableObject); } else { - return editableObject.getCapability('action').perform('save-as'); + return editableObject.getCapability('action').perform('save'); } }; diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index 7c8f45eb81..a67125b9b9 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -171,10 +171,10 @@ define([ "priority": "mandatory" }, { - "key": "save-as", + "key": "save", "category": "conclude-editing", "implementation": SaveAsAction, - "name": "Save As", + "name": "Save", "description": "Save changes made to these objects.", "depends": [ "$injector", From abb47eed369e2b8032d1c2ba6d720c9ecf10e1b8 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 14 Apr 2016 16:30:17 -0700 Subject: [PATCH 22/38] [Documentation] Remove superfluous space https://github.com/nasa/openmct/pull/823/files/ddc241c0d0372c462f82d170ead677e31761b3f0#r59808305 --- docs/src/process/version.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/process/version.md b/docs/src/process/version.md index aec9fe9248..21e0f09351 100644 --- a/docs/src/process/version.md +++ b/docs/src/process/version.md @@ -4,7 +4,7 @@ This document describes semantics and processes for providing version numbers for Open MCT, and additionally provides guidelines for dependent projects developed by the same team. -Versions are incremented at specific points in Open MCT's +Versions are incremented at specific points in Open MCT's [Development Cycle](cycle.md); see that document for a description of sprints and releases. From 06436c488af5f5760e9a72efd94c6a80a55ac60b Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 18 Apr 2016 19:05:46 -0700 Subject: [PATCH 23/38] [Edit Mode] #794 Modified policy to show remove action in context for non-editable domain object types --- platform/commonUI/edit/bundle.js | 12 +++++++- .../policies/EditContextualActionPolicy.js | 28 +++++++++++------ .../EditContextualActionPolicySpec.js | 30 +++++++++++++++++-- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index f2e684a69f..e766d96e56 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -207,7 +207,7 @@ define([ { "category": "action", "implementation": EditContextualActionPolicy, - "depends": ["navigationService"] + "depends": ["navigationService", "editModeBlacklist", "nonEditContextBlacklist"] }, { "category": "action", @@ -274,6 +274,16 @@ define([ { "implementation": EditToolbarRepresenter } + ], + "constants": [ + { + "key":"editModeBlacklist", + "value": ["copy", "follow", "window", "link", "locate"] + }, + { + "key": "nonEditContextBlacklist", + "value": ["copy", "follow", "properties", "move", "link", "remove", "locate"] + } ] } }); diff --git a/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js b/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js index 06fe9de640..6b6c22252e 100644 --- a/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js +++ b/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js @@ -29,17 +29,27 @@ define( /** * Policy controlling whether the context menu is visible when * objects are being edited - * @memberof platform/commonUI/edit + * @param navigationService + * @param editModeBlacklist A blacklist of actions disallowed from + * context menu when navigated object is being edited + * @param nonEditContextBlacklist A blacklist of actions disallowed + * from context menu of non-editable objects, when navigated object + * is being edited * @constructor - * @implements {Policy.} */ - function EditContextualActionPolicy(navigationService) { + function EditContextualActionPolicy(navigationService, editModeBlacklist, nonEditContextBlacklist) { this.navigationService = navigationService; + //The list of objects disallowed on target object when in edit mode - this.editBlacklist = ["copy", "follow", "window"]; + this.editModeBlacklist = editModeBlacklist; //The list of objects disallowed on target object that is not in // edit mode (ie. the context menu in the tree on the LHS). - this.nonEditBlacklist = ["copy", "follow", "properties", "move", "link", "remove"]; + this.nonEditContextBlacklist = nonEditContextBlacklist; + } + + function isParentEditable(object) { + var parent = object.hasCapability("context") && object.getCapability("context").getParent(); + return !!parent && parent.hasCapability("editor"); } EditContextualActionPolicy.prototype.allow = function (action, context) { @@ -48,11 +58,11 @@ define( actionMetadata = action.getMetadata ? action.getMetadata() : {}; if (navigatedObject.hasCapability('editor')) { - if (!selectedObject.hasCapability('editor')){ - //Target is in the context menu - return this.nonEditBlacklist.indexOf(actionMetadata.key) === -1; + if (selectedObject.hasCapability('editor') || isParentEditable(selectedObject)){ + return this.editModeBlacklist.indexOf(actionMetadata.key) === -1; } else { - return this.editBlacklist.indexOf(actionMetadata.key) === -1; + //Target is in the context menu + return this.nonEditContextBlacklist.indexOf(actionMetadata.key) === -1; } } else { return true; diff --git a/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js b/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js index 8dbf2c128c..f7ec5e1709 100644 --- a/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js +++ b/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js @@ -33,13 +33,15 @@ define( context, navigatedObject, mockDomainObject, - metadata; + metadata, + editModeBlacklist = ["copy", "follow", "window", "link", "locate"], + nonEditContextBlacklist = ["copy", "follow", "properties", "move", "link", "remove", "locate"]; beforeEach(function () { navigatedObject = jasmine.createSpyObj("navigatedObject", ["hasCapability"]); navigatedObject.hasCapability.andReturn(false); - mockDomainObject = jasmine.createSpyObj("domainObject", ["hasCapability"]); + mockDomainObject = jasmine.createSpyObj("domainObject", ["hasCapability", "getCapability"]); mockDomainObject.hasCapability.andReturn(false); navigationService = jasmine.createSpyObj("navigationService", ["getNavigation"]); @@ -51,7 +53,7 @@ define( context = {domainObject: mockDomainObject}; - policy = new EditContextualActionPolicy(navigationService); + policy = new EditContextualActionPolicy(navigationService, editModeBlacklist, nonEditContextBlacklist); }); it('Allows all actions when navigated object not in edit mode', function() { @@ -65,6 +67,28 @@ define( expect(policy.allow(mockAction, context)).toBe(true); }); + it('Allows "remove" action when navigated object in edit mode,' + + ' and selected object not editable, but its parent is.', + function() { + var mockParent = jasmine.createSpyObj("parentObject", ["hasCapability"]), + mockContextCapability = jasmine.createSpyObj("contextCapability", ["getParent"]); + + mockParent.hasCapability.andReturn(true); + mockContextCapability.getParent.andReturn(mockParent); + navigatedObject.hasCapability.andReturn(true); + + mockDomainObject.getCapability.andReturn(mockContextCapability); + mockDomainObject.hasCapability.andCallFake(function (capability) { + switch (capability) { + case "editor": return false; + case "context": return true; + } + }); + metadata.key = "remove"; + + expect(policy.allow(mockAction, context)).toBe(true); + }); + it('Disallows "move" action when navigated object in edit mode,' + ' but selected object not in edit mode ', function() { navigatedObject.hasCapability.andReturn(true); From aac5a6d250cfef99633596a3a9548d975f482d18 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 20 Apr 2016 19:18:27 -0700 Subject: [PATCH 24/38] #769 - Removed empty code block and addressed some excessive guard code --- platform/features/table/src/TableConfiguration.js | 7 +++---- .../features/table/src/controllers/MCTTableController.js | 4 +--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js index 59994ff13e..8494d88104 100644 --- a/platform/features/table/src/TableConfiguration.js +++ b/platform/features/table/src/TableConfiguration.js @@ -140,8 +140,7 @@ define( * @private */ TableConfiguration.prototype.defaultColumnConfiguration = function () { - return ((this.domainObject.getModel().configuration || {}).table || - {}).columns; + return ((this.domainObject.getModel().configuration || {}).table || {}).columns || {}; }; /** @@ -176,7 +175,7 @@ define( TableConfiguration.prototype.buildColumnConfiguration = function () { var configuration = {}, //Use existing persisted config, or default it - defaultConfig = this.defaultColumnConfiguration() || {}; + defaultConfig = this.defaultColumnConfiguration(); /** * For each column header, define a configuration value @@ -185,7 +184,7 @@ define( */ this.getHeaders().forEach(function (columnTitle) { configuration[columnTitle] = - typeof (defaultConfig || {})[columnTitle] === 'undefined' ? true : + typeof defaultConfig[columnTitle] === 'undefined' ? true : defaultConfig[columnTitle]; }); diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index fd949f7056..39540ef0b8 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -421,9 +421,7 @@ define( currentColumnLength, largestColumn, largestColumnLength; - if (!row[key]){ - //do nothing, no value for this column; - } else { + if (row[key]){ currentColumn = (row[key]).text; currentColumnLength = (currentColumn && currentColumn.length) ? From 8b390e7fb9bc8dc241d57477acd84ee89ec4469f Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Wed, 20 Apr 2016 19:31:54 -0700 Subject: [PATCH 25/38] Revert "[Tables] Fix to correct sorting in realtime tables" --- docs/src/guide/index.md | 68 +----- platform/features/table/bundle.js | 24 +-- platform/features/table/res/sass/table.scss | 50 ----- .../table/res/templates/mct-table.html | 43 ++-- .../table/res/templates/rt-table.html | 2 +- .../{historical-table.html => table.html} | 2 +- .../features/table/src/TableConfiguration.js | 22 +- .../controllers/HistoricalTableController.js | 70 ------- .../src/controllers/MCTTableController.js | 195 ++++++++++-------- ...oller.js => RTTelemetryTableController.js} | 73 ++++--- .../src/controllers/TableOptionsController.js | 27 +-- .../controllers/TelemetryTableController.js | 94 +++++---- .../features/table/src/directives/MCTTable.js | 45 ---- .../table/test/TableConfigurationSpec.js | 8 +- .../controllers/MCTTableControllerSpec.js | 97 ++------- ...c.js => RTTelemetryTableControllerSpec.js} | 13 +- .../controllers/TableOptionsControllerSpec.js | 22 +- ...pec.js => TelemetryTableControllerSpec.js} | 17 +- 18 files changed, 293 insertions(+), 579 deletions(-) delete mode 100644 platform/features/table/res/sass/table.scss rename platform/features/table/res/templates/{historical-table.html => table.html} (71%) delete mode 100644 platform/features/table/src/controllers/HistoricalTableController.js rename platform/features/table/src/controllers/{RealtimeTableController.js => RTTelemetryTableController.js} (59%) rename platform/features/table/test/controllers/{RealtimeTableControllerSpec.js => RTTelemetryTableControllerSpec.js} (94%) rename platform/features/table/test/controllers/{HistoricalTableControllerSpec.js => TelemetryTableControllerSpec.js} (94%) diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index 7016462a5a..93cb95bb7a 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -6,13 +6,12 @@ Victor Woeltjen September 23, 2015 Document Version 1.1 -Date | Version | Summary of Changes | Author -------------------- | --------- | ------------------------- | --------------- -April 29, 2015 | 0 | Initial Draft | Victor Woeltjen -May 12, 2015 | 0.1 | | Victor Woeltjen -June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen -October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry -April 5, 2016 | 1.2 | Added Mct-table directive | Andrew Henry +Date | Version | Summary of Changes | Author +------------------- | --------- | ----------------------- | --------------- +April 29, 2015 | 0 | Initial Draft | Victor Woeltjen +May 12, 2015 | 0.1 | | Victor Woeltjen +June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen +October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry # Introduction The purpose of this guide is to familiarize software developers with the Open @@ -1601,61 +1600,6 @@ there are items . ] } -## Table - -The `mct-table` directive provides a generic table component, with optional -sorting and filtering capabilities. The table can be pre-populated with data -by setting the `rows` parameter, and it can be updated in real-time using the -`add:row` and `remove:row` broadcast events. The table will expand to occupy -100% of the size of its containing element. The table is highly optimized for -very large data sets. - -### Events - -The table supports two events for notifying that the rows have changed. For -performance reasons, the table does not monitor the content of `rows` -constantly. - -* `add:row`: A `$broadcast` event that will notify the table that a new row -has been added to the table. - -eg. The code below adds a new row, and alerts the table using the `add:row` -event. Sorting and filtering will be applied automatically by the table component. - -``` -$scope.rows.push(newRow); -$scope.$broadcast('add:row', $scope.rows.length-1); -``` - -* `remove:row`: A `$broadcast` event that will notify the table that a row -should be removed from the table. - -eg. The code below removes a row from the rows array, and then alerts the table -to its removal. - -``` -$scope.rows.slice(5, 1); -$scope.$broadcast('remove:row', 5); -``` - -### Parameters - -* `headers`: An array of string values which will constitute the column titles - that appear at the top of the table. Corresponding values are specified in - the rows using the header title provided here. -* `rows`: An array of objects containing row values. Each element in the -array must be an associative array, where the key corresponds to a column header. -* `enableFilter`: A boolean that if true, will enable searching and result -filtering. When enabled, each column will have a text input field that can be -used to filter the table rows in real time. -* `enableSort`: A boolean determining whether rows can be sorted. If true, -sorting will be enabled allowing sorting by clicking on column headers. Only -one column may be sorted at a time. -* `autoScroll`: A boolean value that if true, will cause the table to automatically -scroll to the bottom as new data arrives. Auto-scroll can be disengaged manually -by scrolling away from the bottom of the table, and can also be enabled manually -by scrolling to the bottom of the table rows. - # Services The Open MCT Web platform provides a variety of services which can be retrieved diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js index 0220b7dc6d..77ead67c04 100644 --- a/platform/features/table/bundle.js +++ b/platform/features/table/bundle.js @@ -23,16 +23,16 @@ define([ "./src/directives/MCTTable", - "./src/controllers/RealtimeTableController", - "./src/controllers/HistoricalTableController", + "./src/controllers/RTTelemetryTableController", + "./src/controllers/TelemetryTableController", "./src/controllers/TableOptionsController", '../../commonUI/regions/src/Region', '../../commonUI/browse/src/InspectorRegion', "legacyRegistry" ], function ( MCTTable, - RealtimeTableController, - HistoricalTableController, + RTTelemetryTableController, + TelemetryTableController, TableOptionsController, Region, InspectorRegion, @@ -109,13 +109,13 @@ define([ ], "controllers": [ { - "key": "HistoricalTableController", - "implementation": HistoricalTableController, + "key": "TelemetryTableController", + "implementation": TelemetryTableController, "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] }, { - "key": "RealtimeTableController", - "implementation": RealtimeTableController, + "key": "RTTelemetryTableController", + "implementation": RTTelemetryTableController, "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] }, { @@ -130,7 +130,7 @@ define([ "name": "Historical Table", "key": "table", "glyph": "\ue604", - "templateUrl": "templates/historical-table.html", + "templateUrl": "templates/table.html", "needs": [ "telemetry" ], @@ -161,12 +161,6 @@ define([ "key": "table-options-edit", "templateUrl": "templates/table-options-edit.html" } - ], - "stylesheets": [ - { - "stylesheetUrl": "css/table.css", - "priority": "mandatory" - } ] } }); diff --git a/platform/features/table/res/sass/table.scss b/platform/features/table/res/sass/table.scss deleted file mode 100644 index a79cfac4c6..0000000000 --- a/platform/features/table/res/sass/table.scss +++ /dev/null @@ -1,50 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web 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 Web 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. - *****************************************************************************/ -.sizing-table { - min-width: 100%; - z-index: -1; - visibility: hidden; - position: absolute; - - //Add some padding to allow for decorations such as limits indicator - td { - padding-right: 15px; - padding-left: 10px; - white-space: nowrap; - } -} -.mct-table { - table-layout: fixed; - th { - box-sizing: border-box; - } - tbody { - tr { - position: absolute; - } - td { - white-space: nowrap; - overflow: hidden; - box-sizing: border-box; - } - } -} \ No newline at end of file diff --git a/platform/features/table/res/templates/mct-table.html b/platform/features/table/res/templates/mct-table.html index 7a18388455..5997376587 100644 --- a/platform/features/table/res/templates/mct-table.html +++ b/platform/features/table/res/templates/mct-table.html @@ -1,25 +1,22 @@ -
- - - - - - - -
{{header}}
- {{sizingRow[header].text}} -
- +
+ }"> - +
@@ -42,15 +41,21 @@
{{ visibleRow.contents[header].text }} diff --git a/platform/features/table/res/templates/rt-table.html b/platform/features/table/res/templates/rt-table.html index d35015c96c..326c5b847b 100644 --- a/platform/features/table/res/templates/rt-table.html +++ b/platform/features/table/res/templates/rt-table.html @@ -1,4 +1,4 @@ -
+
+
0) { this.insertSorted(this.$scope.displayRows, row); - - //Resize the columns , then update the rows visible in the table - this.resize([this.$scope.sizingRow, row]) - .then(this.setVisibleRows.bind(this)) - .then(this.scrollToBottom.bind(this)); } + + this.$timeout(this.setElementSizes.bind(this)) + .then(this.scrollToBottom.bind(this)); }; /** - * Handles a row remove event. Rows can be removed as needed using the - * `remove:row` broadcast event. + * Handles a row add event. Rows can be added as needed using the + * `addRow` broadcast event. * @private */ MCTTableController.prototype.removeRow = function (event, rowIndex) { @@ -232,7 +225,7 @@ define( * enabled, reset filters. If sorting is enabled, reset * sorting. */ - MCTTableController.prototype.setHeaders = function (newHeaders) { + MCTTableController.prototype.updateHeaders = function (newHeaders) { if (!newHeaders){ return; } @@ -248,7 +241,7 @@ define( this.$scope.sortColumn = undefined; this.$scope.sortDirection = undefined; } - this.setRows(this.$scope.rows); + this.updateRows(this.$scope.rows); }; /** @@ -256,12 +249,13 @@ define( * for individual rows. */ MCTTableController.prototype.setElementSizes = function () { - var thead = this.thead, - tbody = this.tbody, + var self = this, + thead = this.element.find('thead'), + tbody = this.element.find('tbody'), firstRow = tbody.find('tr'), column = firstRow.find('td'), headerHeight = thead.prop('offsetHeight'), - rowHeight = firstRow.prop('offsetHeight'), + rowHeight = 20, columnWidth, tableWidth = 0, overallHeight = headerHeight + (rowHeight * @@ -278,12 +272,15 @@ define( this.$scope.headerHeight = headerHeight; this.$scope.rowHeight = rowHeight; this.$scope.totalHeight = overallHeight; + this.setVisibleRows(); if (tableWidth > 0) { this.$scope.totalWidth = tableWidth + 'px'; } else { this.$scope.totalWidth = 'none'; } + + this.$scope.overrideRowPositioning = true; }; /** @@ -295,14 +292,21 @@ define( sortKey = this.$scope.sortColumn; function binarySearch(searchArray, searchElement, min, max){ - var sampleAt = Math.floor((max - min) / 2) + min; - + var sampleAt = Math.floor((max - min) / 2) + min, + valA, + valB; if (max < min) { return min; // Element is not in array, min gives direction } - switch(self.sortComparator(searchElement[sortKey].text, - searchArray[sampleAt][sortKey].text)) { + valA = isNaN(searchElement[sortKey].text) ? + searchElement[sortKey].text : + parseFloat(searchElement[sortKey].text); + valB = isNaN(searchArray[sampleAt][sortKey].text) ? + searchArray[sampleAt][sortKey].text : + parseFloat(searchArray[sampleAt][sortKey].text); + + switch(self.sortComparator(valA, valB)) { case -1: return binarySearch(searchArray, searchElement, min, sampleAt - 1); @@ -340,34 +344,8 @@ define( */ MCTTableController.prototype.sortComparator = function (a, b) { var result = 0, - sortDirectionMultiplier, - numberA, - numberB; - /** - * Given a value, if it is a number, or a string representation of a - * number, then return a number representation. Otherwise, return - * the original value. It's a little more robust than using just - * Number() or parseFloat, or isNaN in isolation, all of which are - * fairly inconsistent in their results. - * @param value The value to return as a number. - * @returns {*} The value cast to a Number, or the original value if - * a Number representation is not possible. - */ - function toNumber (value){ - var val = !isNaN(Number(value)) && !isNaN(parseFloat(value)) ? Number(value) : value; - return val; - } + sortDirectionMultiplier; - numberA = toNumber(a); - numberB = toNumber(b); - - //If they're both numbers, then compare them as numbers - if (typeof numberA === "number" && typeof numberB === "number") { - a = numberA; - b = numberB; - } - - //If they're both strings, then ignore case if (typeof a === "string" && typeof b === "string") { a = a.toLowerCase(); b = b.toLowerCase(); @@ -404,7 +382,15 @@ define( } return rowsToSort.sort(function (a, b) { - return self.sortComparator(a[sortKey].text, b[sortKey].text); + //If the values to compare can be compared as + // numbers, do so. String comparison of number + // values can cause inconsistencies + var valA = isNaN(a[sortKey].text) ? a[sortKey].text : + parseFloat(a[sortKey].text), + valB = isNaN(b[sortKey].text) ? b[sortKey].text : + parseFloat(b[sortKey].text); + + return self.sortComparator(valA, valB); }); }; @@ -414,48 +400,74 @@ define( * pre-calculate optimal column sizes without having to render * every row. */ - MCTTableController.prototype.buildLargestRow = function (rows) { - var largestRow = rows.reduce(function (prevLargest, row) { + MCTTableController.prototype.findLargestRow = function (rows) { + var largestRow = rows.reduce(function (largestRow, row) { Object.keys(row).forEach(function (key) { - var currentColumn, - currentColumnLength, - largestColumn, - largestColumnLength; - if (row[key]){ - currentColumn = (row[key]).text; + var currentColumn = row[key].text, currentColumnLength = (currentColumn && currentColumn.length) ? currentColumn.length : - currentColumn; - largestColumn = prevLargest[key] ? prevLargest[key].text : ""; - largestColumnLength = largestColumn.length; + currentColumn, + largestColumn = largestRow[key].text, + largestColumnLength = + (largestColumn && largestColumn.length) ? + largestColumn.length : + largestColumn; - if (currentColumnLength > largestColumnLength) { - prevLargest[key] = JSON.parse(JSON.stringify(row[key])); - } + if (currentColumnLength > largestColumnLength) { + largestRow[key] = JSON.parse(JSON.stringify(row[key])); } }); - return prevLargest; + return largestRow; }, JSON.parse(JSON.stringify(rows[0] || {}))); + + largestRow = JSON.parse(JSON.stringify(largestRow)); + + // Pad with characters to accomodate variable-width fonts, + // and remove characters that would allow word-wrapping. + Object.keys(largestRow).forEach(function (key) { + var padCharacters, + i; + + largestRow[key].text = String(largestRow[key].text); + padCharacters = largestRow[key].text.length / 10; + for (i = 0; i < padCharacters; i++) { + largestRow[key].text = largestRow[key].text + 'W'; + } + largestRow[key].text = largestRow[key].text + .replace(/[ \-_]/g, 'W'); + }); return largestRow; }; /** - * Calculates the widest row in the table, and if necessary, resizes - * the table accordingly - * - * @param rows the rows on which to resize - * @returns {Promise} a promise that will resolve when resizing has - * occurred. + * Calculates the widest row in the table, pads that row, and adds + * it to the table. Allows the table to size itself, then uses this + * as basis for column dimensions. * @private */ - MCTTableController.prototype.resize = function (rows) { - this.$scope.sizingRow = this.buildLargestRow(rows); - return this.$timeout(this.setElementSizes.bind(this)); + MCTTableController.prototype.resize = function (){ + var largestRow = this.findLargestRow(this.$scope.displayRows), + self = this; + this.$scope.visibleRows = [ + { + rowIndex: 0, + offsetY: undefined, + contents: largestRow + } + ]; + + //Wait a timeout to allow digest of previous change to visible + // rows to happen. + this.$timeout(function () { + //Remove temporary padding row used for setting column widths + self.$scope.visibleRows = []; + self.setElementSizes(); + }); }; /** - * @private + * @priate */ MCTTableController.prototype.filterAndSort = function (rows) { var displayRows = rows; @@ -466,21 +478,26 @@ define( if (this.$scope.enableSort) { displayRows = this.sortRows(displayRows.slice(0)); } - return displayRows; + this.$scope.displayRows = displayRows; }; /** * Update rows with new data. If filtering is enabled, rows * will be sorted before display. */ - MCTTableController.prototype.setRows = function (newRows) { + MCTTableController.prototype.updateRows = function (newRows) { + //Reset visible rows because new row data available. + this.$scope.visibleRows = []; + + this.$scope.overrideRowPositioning = false; + //Nothing to show because no columns visible - if (!this.$scope.displayHeaders || !newRows) { + if (!this.$scope.displayHeaders) { return; } - this.$scope.displayRows = this.filterAndSort(newRows || []); - this.resize(newRows).then(this.setVisibleRows.bind(this)); + this.filterAndSort(newRows || []); + this.resize(); }; /** diff --git a/platform/features/table/src/controllers/RealtimeTableController.js b/platform/features/table/src/controllers/RTTelemetryTableController.js similarity index 59% rename from platform/features/table/src/controllers/RealtimeTableController.js rename to platform/features/table/src/controllers/RTTelemetryTableController.js index 3f983207bc..8a61d61b5e 100644 --- a/platform/features/table/src/controllers/RealtimeTableController.js +++ b/platform/features/table/src/controllers/RTTelemetryTableController.js @@ -37,7 +37,7 @@ define( * @param telemetryFormatter * @constructor */ - function RealtimeTableController($scope, telemetryHandler, telemetryFormatter) { + function RTTelemetryTableController($scope, telemetryHandler, telemetryFormatter) { TableController.call(this, $scope, telemetryHandler, telemetryFormatter); $scope.autoScroll = false; @@ -66,35 +66,58 @@ define( }); } - RealtimeTableController.prototype = Object.create(TableController.prototype); + RTTelemetryTableController.prototype = Object.create(TableController.prototype); /** - * Overrides method on TelemetryTableController providing handling - * for realtime data. + Override the subscribe function defined on the parent controller in + order to handle realtime telemetry instead of historical. */ - RealtimeTableController.prototype.addRealtimeData = function() { - var self = this, - datum, - row; - this.handle.getTelemetryObjects().forEach(function (telemetryObject){ - datum = self.handle.getDatum(telemetryObject); - if (datum) { - //Populate row values from telemetry datum - row = self.table.getRowValues(telemetryObject, datum); - self.$scope.rows.push(row); - - //Inform table that a new row has been added - if (self.$scope.rows.length > self.maxRows) { - self.$scope.$broadcast('remove:row', 0); - self.$scope.rows.shift(); - } - - self.$scope.$broadcast('add:row', - self.$scope.rows.length - 1); - } + RTTelemetryTableController.prototype.subscribe = function () { + var self = this; + self.$scope.rows = undefined; + (this.subscriptions || []).forEach(function (unsubscribe){ + unsubscribe(); }); + + if (this.handle) { + this.handle.unsubscribe(); + } + + function updateData(){ + var datum, + row; + self.handle.getTelemetryObjects().forEach(function (telemetryObject){ + datum = self.handle.getDatum(telemetryObject); + if (datum) { + row = self.table.getRowValues(telemetryObject, datum); + if (!self.$scope.rows){ + self.$scope.rows = [row]; + self.$scope.$digest(); + } else { + self.$scope.rows.push(row); + + if (self.$scope.rows.length > self.maxRows) { + self.$scope.$broadcast('remove:row', 0); + self.$scope.rows.shift(); + } + + self.$scope.$broadcast('add:row', + self.$scope.rows.length - 1); + } + } + }); + + } + + this.handle = this.$scope.domainObject && this.telemetryHandler.handle( + this.$scope.domainObject, + updateData, + true // Lossless + ); + + this.setup(); }; - return RealtimeTableController; + return RTTelemetryTableController; } ); diff --git a/platform/features/table/src/controllers/TableOptionsController.js b/platform/features/table/src/controllers/TableOptionsController.js index a28411e7d0..c3b479073c 100644 --- a/platform/features/table/src/controllers/TableOptionsController.js +++ b/platform/features/table/src/controllers/TableOptionsController.js @@ -51,29 +51,13 @@ define( this.$scope = $scope; this.domainObject = $scope.domainObject; - this.listeners = []; $scope.columnsForm = {}; - function unlisten() { - self.listeners.forEach(function (listener) { - listener(); - }); - } - - $scope.$watch('domainObject', function(domainObject) { - unlisten(); - self.populateForm(domainObject.getModel()); - - self.listeners.push(self.domainObject.getCapability('mutation').listen(function (model) { - self.populateForm(model); - })); + this.domainObject.getCapability('mutation').listen(function (model) { + self.populateForm(model); }); - /** - * Maintain a configuration object on scope that stores column - * configuration. On change, synchronize with object model. - */ $scope.$watchCollection('configuration.table.columns', function (columns){ if (columns){ self.domainObject.useCapability('mutation', function (model) { @@ -83,11 +67,6 @@ define( } }); - /** - * Destroy all mutation listeners - */ - $scope.$on('$destroy', unlisten); - } TableOptionsController.prototype.populateForm = function (model) { @@ -107,7 +86,7 @@ define( 'key': key }); }); - this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration || {})); + this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration)); }; return TableOptionsController; diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 36f54b1ac6..e579c5eeb8 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -52,15 +52,19 @@ define( this.$scope = $scope; this.columns = {}; //Range and Domain columns this.handle = undefined; + //this.pending = false; this.telemetryHandler = telemetryHandler; this.table = new TableConfiguration($scope.domainObject, telemetryFormatter); this.changeListeners = []; - $scope.rows = []; + $scope.rows = undefined; // Subscribe to telemetry when a domain object becomes available - this.$scope.$watch('domainObject', function(){ + this.$scope.$watch('domainObject', function(domainObject){ + if (!domainObject) + return; + self.subscribe(); self.registerChangeListeners(); }); @@ -69,24 +73,16 @@ define( this.$scope.$on("$destroy", this.destroy.bind(this)); } - /** - * @private - */ - TelemetryTableController.prototype.unregisterChangeListeners = function () { - this.changeListeners.forEach(function (listener) { - return listener && listener(); - }); - this.changeListeners = []; - }; - /** * Defer registration of change listeners until domain object is * available in order to avoid race conditions * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { - this.unregisterChangeListeners(); - + this.changeListeners.forEach(function (listener) { + return listener && listener(); + }); + this.changeListeners = []; // When composition changes, re-subscribe to the various // telemetry subscriptions this.changeListeners.push(this.$scope.$watchCollection( @@ -107,37 +103,25 @@ define( } }; - /** - * Function for handling realtime data when it is available. This - * will be called by the telemetry framework when new data is - * available. - * - * Method should be overridden by specializing class. - */ - TelemetryTableController.prototype.addRealtimeData = function () { - }; - - /** - * Function for handling historical data. Will be called by - * telemetry framework when requested historical data is available. - * Should be overridden by specializing class. - */ - TelemetryTableController.prototype.addHistoricalData = function () { - }; - /** Create a new subscription. This can be overridden by children to change default behaviour (which is to retrieve historical telemetry only). */ TelemetryTableController.prototype.subscribe = function () { + var self = this; + if (this.handle) { this.handle.unsubscribe(); } + //Noop because not supporting realtime data right now + function noop(){ + } + this.handle = this.$scope.domainObject && this.telemetryHandler.handle( this.$scope.domainObject, - this.addRealtimeData.bind(this), + noop, true // Lossless ); @@ -146,6 +130,28 @@ define( this.setup(); }; + /** + * Populates historical data on scope when it becomes available + * @private + */ + TelemetryTableController.prototype.addHistoricalData = function () { + var rowData = [], + self = this; + + this.handle.getTelemetryObjects().forEach(function (telemetryObject){ + var series = self.handle.getSeries(telemetryObject) || {}, + pointCount = series.getPointCount ? series.getPointCount() : 0, + i = 0; + + for (; i < pointCount; i++) { + rowData.push(self.table.getRowValues(telemetryObject, + self.handle.makeDatum(telemetryObject, series, i))); + } + }); + + this.$scope.rows = rowData; + }; + /** * Setup table columns based on domain object metadata */ @@ -156,9 +162,7 @@ define( if (handle) { handle.promiseTelemetryObjects().then(function () { - self.$scope.headers = []; - self.$scope.rows = []; - table.populateColumns(handle.getMetadata()); + table.buildColumns(handle.getMetadata()); self.filterColumns(); @@ -172,14 +176,26 @@ define( } }; + /** + * @private + * @param object The object for which data is available (table may + * be composed of multiple objects) + * @param datum The data received from the telemetry source + */ + TelemetryTableController.prototype.updateRows = function (object, datum) { + this.$scope.rows.push(this.table.getRowValues(object, datum)); + }; + /** * When column configuration changes, update the visible headers * accordingly. * @private */ - TelemetryTableController.prototype.filterColumns = function () { - var columnConfig = this.table.buildColumnConfiguration(); - + TelemetryTableController.prototype.filterColumns = function (columnConfig) { + if (!columnConfig){ + columnConfig = this.table.getColumnConfiguration(); + this.table.saveColumnConfiguration(columnConfig); + } //Populate headers with visible columns (determined by configuration) this.$scope.headers = Object.keys(columnConfig).filter(function (column) { return columnConfig[column]; diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js index 2d61669a2e..575e830395 100644 --- a/platform/features/table/src/directives/MCTTable.js +++ b/platform/features/table/src/directives/MCTTable.js @@ -12,51 +12,6 @@ define( * Defines a generic 'Table' component. The table can be populated * en-masse by setting the rows attribute, or rows can be added as * needed via a broadcast 'addRow' event. - * - * This directive accepts parameters specifying header and row - * content, as well as some additional options. - * - * Two broadcast events for notifying the table that the rows have - * changed. For performance reasons, the table does not monitor the - * content of `rows` constantly. - * - 'add:row': A $broadcast event that will notify the table that - * a new row has been added to the table. - * eg. - *

-         * $scope.rows.push(newRow);
-         * $scope.$broadcast('add:row', $scope.rows.length-1);
-         * 
- * The code above adds a new row, and alerts the table using the - * add:row event. Sorting and filtering will be applied - * automatically by the table component. - * - * - 'remove:row': A $broadcast event that will notify the table that a - * row should be removed from the table. - * eg. - *

-         * $scope.rows.slice(5, 1);
-         * $scope.$broadcast('remove:row', 5);
-         * 
- * The code above removes a row from the rows array, and then alerts - * the table to its removal. - * - * @memberof platform/features/table - * @param {string[]} headers The column titles to appear at the top - * of the table. Corresponding values are specified in the rows - * using the header title provided here. - * @param {Object[]} rows The row content. Each row is an object - * with key-value pairs where the key corresponds to a header - * specified in the headers parameter. - * @param {boolean} enableFilter If true, values will be searchable - * and results filtered - * @param {boolean} enableSort If true, sorting will be enabled - * allowing sorting by clicking on column headers - * @param {boolean} autoScroll If true, table will automatically - * scroll to the bottom as new data arrives. Auto-scroll can be - * disengaged manually by scrolling away from the bottom of the - * table, and can also be enabled manually by scrolling to the bottom of - * the table rows. - * * @constructor */ function MCTTable($timeout) { diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js index bfc6f50edb..86a18aee5a 100644 --- a/platform/features/table/test/TableConfigurationSpec.js +++ b/platform/features/table/test/TableConfigurationSpec.js @@ -116,10 +116,10 @@ define( }]; beforeEach(function() { - table.populateColumns(metadata); + table.buildColumns(metadata); }); - it("populates columns", function() { + it("populates the columns attribute", function() { expect(table.columns.length).toBe(5); }); @@ -141,7 +141,7 @@ define( it("Provides a default configuration with all columns" + " visible", function() { - var configuration = table.buildColumnConfiguration(); + var configuration = table.getColumnConfiguration(); expect(configuration).toBeDefined(); expect(Object.keys(configuration).every(function(key){ @@ -160,7 +160,7 @@ define( }; mockModel.configuration = modelConfig; - tableConfig = table.buildColumnConfiguration(); + tableConfig = table.getColumnConfiguration(); expect(tableConfig).toBeDefined(); expect(tableConfig['Range 1']).toBe(false); diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 175d8fed0a..5e38c7e651 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -58,18 +58,15 @@ define( mockElement = jasmine.createSpyObj('element', [ 'find', - 'prop', 'on' ]); mockElement.find.andReturn(mockElement); - mockElement.prop.andReturn(0); mockScope.displayHeaders = true; mockTimeout = jasmine.createSpy('$timeout'); mockTimeout.andReturn(promise(undefined)); controller = new MCTTableController(mockScope, mockTimeout, mockElement); - spyOn(controller, 'setVisibleRows'); }); it('Reacts to changes to filters, headers, and rows', function() { @@ -118,7 +115,7 @@ define( }); it('Sets rows on scope when rows change', function() { - controller.setRows(testRows); + controller.updateRows(testRows); expect(mockScope.displayRows.length).toBe(3); expect(mockScope.displayRows).toEqual(testRows); }); @@ -130,7 +127,7 @@ define( 'col2': {'text': 'ghi'}, 'col3': {'text': 'row3 col3'} }; - controller.setRows(testRows); + controller.updateRows(testRows); expect(mockScope.displayRows.length).toBe(3); testRows.push(row4); addRowFunc(undefined, 3); @@ -139,8 +136,10 @@ define( it('Supports removing rows individually', function() { var removeRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-1].args[1]; - controller.setRows(testRows); + controller.updateRows(testRows); expect(mockScope.displayRows.length).toBe(3); + spyOn(controller, 'setVisibleRows'); + //controller.setVisibleRows.andReturn(undefined); removeRowFunc(undefined, 2); expect(mockScope.displayRows.length).toBe(2); expect(controller.setVisibleRows).toHaveBeenCalled(); @@ -180,54 +179,7 @@ define( expect(sortedRows[2].col2.text).toEqual('abc'); }); - it('correctly sorts rows of differing types', function () { - mockScope.sortColumn = 'col2'; - mockScope.sortDirection = 'desc'; - - testRows.push({ - 'col1': {'text': 'row4 col1'}, - 'col2': {'text': '123'}, - 'col3': {'text': 'row4 col3'} - }); - testRows.push({ - 'col1': {'text': 'row5 col1'}, - 'col2': {'text': '456'}, - 'col3': {'text': 'row5 col3'} - }); - testRows.push({ - 'col1': {'text': 'row5 col1'}, - 'col2': {'text': ''}, - 'col3': {'text': 'row5 col3'} - }); - - sortedRows = controller.sortRows(testRows); - expect(sortedRows[0].col2.text).toEqual('ghi'); - expect(sortedRows[1].col2.text).toEqual('def'); - expect(sortedRows[2].col2.text).toEqual('abc'); - - expect(sortedRows[sortedRows.length-3].col2.text).toEqual('456'); - expect(sortedRows[sortedRows.length-2].col2.text).toEqual('123'); - expect(sortedRows[sortedRows.length-1].col2.text).toEqual(''); - }); - - describe('The sort comparator', function () { - it('Correctly sorts different data types', function () { - var val1 = "", - val2 = "1", - val3 = "2016-04-05 18:41:30.713Z", - val4 = "1.1", - val5 = "8.945520958175627e-13"; - mockScope.sortDirection = "asc"; - - expect(controller.sortComparator(val1, val2)).toEqual(-1); - expect(controller.sortComparator(val3, val1)).toEqual(1); - expect(controller.sortComparator(val3, val2)).toEqual(1); - expect(controller.sortComparator(val4, val2)).toEqual(1); - expect(controller.sortComparator(val2, val5)).toEqual(1); - }); - }); - - describe('Adding new rows', function () { + describe('Adding new rows', function() { var row4, row5, row6; @@ -258,20 +210,20 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); mockScope.rows.push(row4); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[4].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); //Add a duplicate row mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); }); @@ -287,18 +239,18 @@ define( mockScope.displayRows = controller.filterRows(testRows); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows[1].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows.length).toBe(2); //Row was not added because does not match filter }); it('Adds new rows at the correct sort position when' + - ' not sorted ', function () { + ' not sorted ', function() { mockScope.sortColumn = undefined; mockScope.sortDirection = undefined; mockScope.filters = {}; @@ -306,33 +258,14 @@ define( mockScope.displayRows = testRows.slice(0); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[3].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length-1); + controller.newRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); }); - it('Resizes columns if length of any columns in new' + - ' row exceeds corresponding existing column', function() { - var row7 = { - 'col1': {'text': 'row6 col1'}, - 'col2': {'text': 'some longer string'}, - 'col3': {'text': 'row6 col3'} - }; - - mockScope.sortColumn = undefined; - mockScope.sortDirection = undefined; - mockScope.filters = {}; - - mockScope.displayRows = testRows.slice(0); - - mockScope.rows.push(row7); - controller.addRow(undefined, mockScope.rows.length-1); - expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); - }); - }); }); diff --git a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js b/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js similarity index 94% rename from platform/features/table/test/controllers/RealtimeTableControllerSpec.js rename to platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js index e0b6978d88..59911d1771 100644 --- a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js +++ b/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js @@ -23,7 +23,7 @@ define( [ - "../../src/controllers/RealtimeTableController" + "../../src/controllers/RTTelemetryTableController" ], function (TableController) { "use strict"; @@ -77,14 +77,14 @@ define( mockTable = jasmine.createSpyObj('table', [ - 'populateColumns', - 'buildColumnConfiguration', + 'buildColumns', + 'getColumnConfiguration', 'getRowValues', 'saveColumnConfiguration' ] ); mockTable.columns = []; - mockTable.buildColumnConfiguration.andReturn(mockConfiguration); + mockTable.getColumnConfiguration.andReturn(mockConfiguration); mockTable.getRowValues.andReturn(mockTableRow); mockDomainObject= jasmine.createSpyObj('domainObject', [ @@ -107,16 +107,13 @@ define( 'unsubscribe', 'getDatum', 'promiseTelemetryObjects', - 'getTelemetryObjects', - 'request' + 'getTelemetryObjects' ]); - // Arbitrary array with non-zero length, contents are not // used by mocks mockTelemetryHandle.getTelemetryObjects.andReturn([{}]); mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); mockTelemetryHandle.getDatum.andReturn({}); - mockTelemetryHandle.request.andReturn(promise(undefined)); mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ 'handle' diff --git a/platform/features/table/test/controllers/TableOptionsControllerSpec.js b/platform/features/table/test/controllers/TableOptionsControllerSpec.js index fd6d8b43fe..9de96b5f52 100644 --- a/platform/features/table/test/controllers/TableOptionsControllerSpec.js +++ b/platform/features/table/test/controllers/TableOptionsControllerSpec.js @@ -47,36 +47,18 @@ define( 'listen' ]); mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability', - 'getModel' + 'getCapability' ]); mockDomainObject.getCapability.andReturn(mockCapability); - mockDomainObject.getModel.andReturn({}); - mockScope = jasmine.createSpyObj('scope', [ - '$watchCollection', - '$watch', - '$on' + '$watchCollection' ]); mockScope.domainObject = mockDomainObject; controller = new TableOptionsController(mockScope); }); - it('Listens for changing domain object', function() { - expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function)); - }); - - it('On destruction of controller, destroys listeners', function() { - var unlistenFunc = jasmine.createSpy("unlisten"); - controller.listeners.push(unlistenFunc); - expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function)); - mockScope.$on.mostRecentCall.args[1](); - expect(unlistenFunc).toHaveBeenCalled(); - }); - it('Registers a listener for mutation events on the object', function() { - mockScope.$watch.mostRecentCall.args[1](mockDomainObject); expect(mockCapability.listen).toHaveBeenCalled(); }); diff --git a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js similarity index 94% rename from platform/features/table/test/controllers/HistoricalTableControllerSpec.js rename to platform/features/table/test/controllers/TelemetryTableControllerSpec.js index f000529467..03f62f11e3 100644 --- a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js +++ b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js @@ -23,7 +23,7 @@ define( [ - "../../src/controllers/HistoricalTableController" + "../../src/controllers/TelemetryTableController" ], function (TableController) { "use strict"; @@ -73,14 +73,14 @@ define( mockTable = jasmine.createSpyObj('table', [ - 'populateColumns', - 'buildColumnConfiguration', + 'buildColumns', + 'getColumnConfiguration', 'getRowValues', 'saveColumnConfiguration' ] ); mockTable.columns = []; - mockTable.buildColumnConfiguration.andReturn(mockConfiguration); + mockTable.getColumnConfiguration.andReturn(mockConfiguration); mockDomainObject= jasmine.createSpyObj('domainObject', [ 'getCapability', @@ -126,18 +126,21 @@ define( expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled(); }); - describe('makes use of the table', function () { + describe('the controller makes use of the table', function () { it('to create column definitions from telemetry' + ' metadata', function () { controller.setup(); - expect(mockTable.populateColumns).toHaveBeenCalled(); + expect(mockTable.buildColumns).toHaveBeenCalled(); }); it('to create column configuration, which is written to the' + ' object model', function () { + var mockModel = {}; + controller.setup(); - expect(mockTable.buildColumnConfiguration).toHaveBeenCalled(); + expect(mockTable.getColumnConfiguration).toHaveBeenCalled(); + expect(mockTable.saveColumnConfiguration).toHaveBeenCalled(); }); }); From 6bf1ef5bcc8ee956f03ba5c2ae92bde2b0dda7a3 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Fri, 22 Apr 2016 09:44:34 -0700 Subject: [PATCH 26/38] Revert "Revert "[Tables] Fix to correct sorting in realtime tables"" --- docs/src/guide/index.md | 68 +++++- platform/features/table/bundle.js | 24 ++- platform/features/table/res/sass/table.scss | 50 +++++ .../{table.html => historical-table.html} | 2 +- .../table/res/templates/mct-table.html | 43 ++-- .../table/res/templates/rt-table.html | 2 +- .../features/table/src/TableConfiguration.js | 22 +- .../controllers/HistoricalTableController.js | 70 +++++++ .../src/controllers/MCTTableController.js | 197 ++++++++---------- ...ntroller.js => RealtimeTableController.js} | 69 ++---- .../src/controllers/TableOptionsController.js | 27 ++- .../controllers/TelemetryTableController.js | 94 ++++----- .../features/table/src/directives/MCTTable.js | 45 ++++ .../table/test/TableConfigurationSpec.js | 8 +- ...ec.js => HistoricalTableControllerSpec.js} | 17 +- .../controllers/MCTTableControllerSpec.js | 97 +++++++-- ...Spec.js => RealtimeTableControllerSpec.js} | 13 +- .../controllers/TableOptionsControllerSpec.js | 22 +- 18 files changed, 578 insertions(+), 292 deletions(-) create mode 100644 platform/features/table/res/sass/table.scss rename platform/features/table/res/templates/{table.html => historical-table.html} (71%) create mode 100644 platform/features/table/src/controllers/HistoricalTableController.js rename platform/features/table/src/controllers/{RTTelemetryTableController.js => RealtimeTableController.js} (59%) rename platform/features/table/test/controllers/{TelemetryTableControllerSpec.js => HistoricalTableControllerSpec.js} (94%) rename platform/features/table/test/controllers/{RTTelemetryTableControllerSpec.js => RealtimeTableControllerSpec.js} (94%) diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index 93cb95bb7a..7016462a5a 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -6,12 +6,13 @@ Victor Woeltjen September 23, 2015 Document Version 1.1 -Date | Version | Summary of Changes | Author -------------------- | --------- | ----------------------- | --------------- -April 29, 2015 | 0 | Initial Draft | Victor Woeltjen -May 12, 2015 | 0.1 | | Victor Woeltjen -June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen -October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry +Date | Version | Summary of Changes | Author +------------------- | --------- | ------------------------- | --------------- +April 29, 2015 | 0 | Initial Draft | Victor Woeltjen +May 12, 2015 | 0.1 | | Victor Woeltjen +June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen +October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry +April 5, 2016 | 1.2 | Added Mct-table directive | Andrew Henry # Introduction The purpose of this guide is to familiarize software developers with the Open @@ -1600,6 +1601,61 @@ there are items . ] } +## Table + +The `mct-table` directive provides a generic table component, with optional +sorting and filtering capabilities. The table can be pre-populated with data +by setting the `rows` parameter, and it can be updated in real-time using the +`add:row` and `remove:row` broadcast events. The table will expand to occupy +100% of the size of its containing element. The table is highly optimized for +very large data sets. + +### Events + +The table supports two events for notifying that the rows have changed. For +performance reasons, the table does not monitor the content of `rows` +constantly. + +* `add:row`: A `$broadcast` event that will notify the table that a new row +has been added to the table. + +eg. The code below adds a new row, and alerts the table using the `add:row` +event. Sorting and filtering will be applied automatically by the table component. + +``` +$scope.rows.push(newRow); +$scope.$broadcast('add:row', $scope.rows.length-1); +``` + +* `remove:row`: A `$broadcast` event that will notify the table that a row +should be removed from the table. + +eg. The code below removes a row from the rows array, and then alerts the table +to its removal. + +``` +$scope.rows.slice(5, 1); +$scope.$broadcast('remove:row', 5); +``` + +### Parameters + +* `headers`: An array of string values which will constitute the column titles + that appear at the top of the table. Corresponding values are specified in + the rows using the header title provided here. +* `rows`: An array of objects containing row values. Each element in the +array must be an associative array, where the key corresponds to a column header. +* `enableFilter`: A boolean that if true, will enable searching and result +filtering. When enabled, each column will have a text input field that can be +used to filter the table rows in real time. +* `enableSort`: A boolean determining whether rows can be sorted. If true, +sorting will be enabled allowing sorting by clicking on column headers. Only +one column may be sorted at a time. +* `autoScroll`: A boolean value that if true, will cause the table to automatically +scroll to the bottom as new data arrives. Auto-scroll can be disengaged manually +by scrolling away from the bottom of the table, and can also be enabled manually +by scrolling to the bottom of the table rows. + # Services The Open MCT Web platform provides a variety of services which can be retrieved diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js index 77ead67c04..0220b7dc6d 100644 --- a/platform/features/table/bundle.js +++ b/platform/features/table/bundle.js @@ -23,16 +23,16 @@ define([ "./src/directives/MCTTable", - "./src/controllers/RTTelemetryTableController", - "./src/controllers/TelemetryTableController", + "./src/controllers/RealtimeTableController", + "./src/controllers/HistoricalTableController", "./src/controllers/TableOptionsController", '../../commonUI/regions/src/Region', '../../commonUI/browse/src/InspectorRegion', "legacyRegistry" ], function ( MCTTable, - RTTelemetryTableController, - TelemetryTableController, + RealtimeTableController, + HistoricalTableController, TableOptionsController, Region, InspectorRegion, @@ -109,13 +109,13 @@ define([ ], "controllers": [ { - "key": "TelemetryTableController", - "implementation": TelemetryTableController, + "key": "HistoricalTableController", + "implementation": HistoricalTableController, "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] }, { - "key": "RTTelemetryTableController", - "implementation": RTTelemetryTableController, + "key": "RealtimeTableController", + "implementation": RealtimeTableController, "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] }, { @@ -130,7 +130,7 @@ define([ "name": "Historical Table", "key": "table", "glyph": "\ue604", - "templateUrl": "templates/table.html", + "templateUrl": "templates/historical-table.html", "needs": [ "telemetry" ], @@ -161,6 +161,12 @@ define([ "key": "table-options-edit", "templateUrl": "templates/table-options-edit.html" } + ], + "stylesheets": [ + { + "stylesheetUrl": "css/table.css", + "priority": "mandatory" + } ] } }); diff --git a/platform/features/table/res/sass/table.scss b/platform/features/table/res/sass/table.scss new file mode 100644 index 0000000000..a79cfac4c6 --- /dev/null +++ b/platform/features/table/res/sass/table.scss @@ -0,0 +1,50 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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. + *****************************************************************************/ +.sizing-table { + min-width: 100%; + z-index: -1; + visibility: hidden; + position: absolute; + + //Add some padding to allow for decorations such as limits indicator + td { + padding-right: 15px; + padding-left: 10px; + white-space: nowrap; + } +} +.mct-table { + table-layout: fixed; + th { + box-sizing: border-box; + } + tbody { + tr { + position: absolute; + } + td { + white-space: nowrap; + overflow: hidden; + box-sizing: border-box; + } + } +} \ No newline at end of file diff --git a/platform/features/table/res/templates/table.html b/platform/features/table/res/templates/historical-table.html similarity index 71% rename from platform/features/table/res/templates/table.html rename to platform/features/table/res/templates/historical-table.html index c63f6d63fc..9917c41dcc 100644 --- a/platform/features/table/res/templates/table.html +++ b/platform/features/table/res/templates/historical-table.html @@ -1,4 +1,4 @@ -
+
- +
+ + + + + + +
{{header}}
+ {{sizingRow[header].text}} +
+ + }"> - +
@@ -41,21 +42,15 @@
{{ visibleRow.contents[header].text }} diff --git a/platform/features/table/res/templates/rt-table.html b/platform/features/table/res/templates/rt-table.html index 326c5b847b..d35015c96c 100644 --- a/platform/features/table/res/templates/rt-table.html +++ b/platform/features/table/res/templates/rt-table.html @@ -1,4 +1,4 @@ -
+
0) { - this.insertSorted(this.$scope.displayRows, row); - } - this.$timeout(this.setElementSizes.bind(this)) - .then(this.scrollToBottom.bind(this)); + //Does the row pass the current filter? + if (this.filterRows([row]).length === 1) { + //Insert the row into the correct position in the array + this.insertSorted(this.$scope.displayRows, row); + + //Resize the columns , then update the rows visible in the table + this.resize([this.$scope.sizingRow, row]) + .then(this.setVisibleRows.bind(this)) + .then(this.scrollToBottom.bind(this)); + } }; /** - * Handles a row add event. Rows can be added as needed using the - * `addRow` broadcast event. + * Handles a row remove event. Rows can be removed as needed using the + * `remove:row` broadcast event. * @private */ MCTTableController.prototype.removeRow = function (event, rowIndex) { @@ -225,7 +232,7 @@ define( * enabled, reset filters. If sorting is enabled, reset * sorting. */ - MCTTableController.prototype.updateHeaders = function (newHeaders) { + MCTTableController.prototype.setHeaders = function (newHeaders) { if (!newHeaders){ return; } @@ -241,7 +248,7 @@ define( this.$scope.sortColumn = undefined; this.$scope.sortDirection = undefined; } - this.updateRows(this.$scope.rows); + this.setRows(this.$scope.rows); }; /** @@ -249,13 +256,12 @@ define( * for individual rows. */ MCTTableController.prototype.setElementSizes = function () { - var self = this, - thead = this.element.find('thead'), - tbody = this.element.find('tbody'), + var thead = this.thead, + tbody = this.tbody, firstRow = tbody.find('tr'), column = firstRow.find('td'), headerHeight = thead.prop('offsetHeight'), - rowHeight = 20, + rowHeight = firstRow.prop('offsetHeight'), columnWidth, tableWidth = 0, overallHeight = headerHeight + (rowHeight * @@ -272,15 +278,12 @@ define( this.$scope.headerHeight = headerHeight; this.$scope.rowHeight = rowHeight; this.$scope.totalHeight = overallHeight; - this.setVisibleRows(); if (tableWidth > 0) { this.$scope.totalWidth = tableWidth + 'px'; } else { this.$scope.totalWidth = 'none'; } - - this.$scope.overrideRowPositioning = true; }; /** @@ -292,21 +295,14 @@ define( sortKey = this.$scope.sortColumn; function binarySearch(searchArray, searchElement, min, max){ - var sampleAt = Math.floor((max - min) / 2) + min, - valA, - valB; + var sampleAt = Math.floor((max - min) / 2) + min; + if (max < min) { return min; // Element is not in array, min gives direction } - valA = isNaN(searchElement[sortKey].text) ? - searchElement[sortKey].text : - parseFloat(searchElement[sortKey].text); - valB = isNaN(searchArray[sampleAt][sortKey].text) ? - searchArray[sampleAt][sortKey].text : - parseFloat(searchArray[sampleAt][sortKey].text); - - switch(self.sortComparator(valA, valB)) { + switch(self.sortComparator(searchElement[sortKey].text, + searchArray[sampleAt][sortKey].text)) { case -1: return binarySearch(searchArray, searchElement, min, sampleAt - 1); @@ -344,8 +340,34 @@ define( */ MCTTableController.prototype.sortComparator = function (a, b) { var result = 0, - sortDirectionMultiplier; + sortDirectionMultiplier, + numberA, + numberB; + /** + * Given a value, if it is a number, or a string representation of a + * number, then return a number representation. Otherwise, return + * the original value. It's a little more robust than using just + * Number() or parseFloat, or isNaN in isolation, all of which are + * fairly inconsistent in their results. + * @param value The value to return as a number. + * @returns {*} The value cast to a Number, or the original value if + * a Number representation is not possible. + */ + function toNumber (value){ + var val = !isNaN(Number(value)) && !isNaN(parseFloat(value)) ? Number(value) : value; + return val; + } + numberA = toNumber(a); + numberB = toNumber(b); + + //If they're both numbers, then compare them as numbers + if (typeof numberA === "number" && typeof numberB === "number") { + a = numberA; + b = numberB; + } + + //If they're both strings, then ignore case if (typeof a === "string" && typeof b === "string") { a = a.toLowerCase(); b = b.toLowerCase(); @@ -382,15 +404,7 @@ define( } return rowsToSort.sort(function (a, b) { - //If the values to compare can be compared as - // numbers, do so. String comparison of number - // values can cause inconsistencies - var valA = isNaN(a[sortKey].text) ? a[sortKey].text : - parseFloat(a[sortKey].text), - valB = isNaN(b[sortKey].text) ? b[sortKey].text : - parseFloat(b[sortKey].text); - - return self.sortComparator(valA, valB); + return self.sortComparator(a[sortKey].text, b[sortKey].text); }); }; @@ -400,74 +414,48 @@ define( * pre-calculate optimal column sizes without having to render * every row. */ - MCTTableController.prototype.findLargestRow = function (rows) { - var largestRow = rows.reduce(function (largestRow, row) { + MCTTableController.prototype.buildLargestRow = function (rows) { + var largestRow = rows.reduce(function (prevLargest, row) { Object.keys(row).forEach(function (key) { - var currentColumn = row[key].text, + var currentColumn, + currentColumnLength, + largestColumn, + largestColumnLength; + if (row[key]){ + currentColumn = (row[key]).text; currentColumnLength = (currentColumn && currentColumn.length) ? currentColumn.length : - currentColumn, - largestColumn = largestRow[key].text, - largestColumnLength = - (largestColumn && largestColumn.length) ? - largestColumn.length : - largestColumn; + currentColumn; + largestColumn = prevLargest[key] ? prevLargest[key].text : ""; + largestColumnLength = largestColumn.length; - if (currentColumnLength > largestColumnLength) { - largestRow[key] = JSON.parse(JSON.stringify(row[key])); + if (currentColumnLength > largestColumnLength) { + prevLargest[key] = JSON.parse(JSON.stringify(row[key])); + } } }); - return largestRow; + return prevLargest; }, JSON.parse(JSON.stringify(rows[0] || {}))); - - largestRow = JSON.parse(JSON.stringify(largestRow)); - - // Pad with characters to accomodate variable-width fonts, - // and remove characters that would allow word-wrapping. - Object.keys(largestRow).forEach(function (key) { - var padCharacters, - i; - - largestRow[key].text = String(largestRow[key].text); - padCharacters = largestRow[key].text.length / 10; - for (i = 0; i < padCharacters; i++) { - largestRow[key].text = largestRow[key].text + 'W'; - } - largestRow[key].text = largestRow[key].text - .replace(/[ \-_]/g, 'W'); - }); return largestRow; }; /** - * Calculates the widest row in the table, pads that row, and adds - * it to the table. Allows the table to size itself, then uses this - * as basis for column dimensions. + * Calculates the widest row in the table, and if necessary, resizes + * the table accordingly + * + * @param rows the rows on which to resize + * @returns {Promise} a promise that will resolve when resizing has + * occurred. * @private */ - MCTTableController.prototype.resize = function (){ - var largestRow = this.findLargestRow(this.$scope.displayRows), - self = this; - this.$scope.visibleRows = [ - { - rowIndex: 0, - offsetY: undefined, - contents: largestRow - } - ]; - - //Wait a timeout to allow digest of previous change to visible - // rows to happen. - this.$timeout(function () { - //Remove temporary padding row used for setting column widths - self.$scope.visibleRows = []; - self.setElementSizes(); - }); + MCTTableController.prototype.resize = function (rows) { + this.$scope.sizingRow = this.buildLargestRow(rows); + return this.$timeout(this.setElementSizes.bind(this)); }; /** - * @priate + * @private */ MCTTableController.prototype.filterAndSort = function (rows) { var displayRows = rows; @@ -478,26 +466,21 @@ define( if (this.$scope.enableSort) { displayRows = this.sortRows(displayRows.slice(0)); } - this.$scope.displayRows = displayRows; + return displayRows; }; /** * Update rows with new data. If filtering is enabled, rows * will be sorted before display. */ - MCTTableController.prototype.updateRows = function (newRows) { - //Reset visible rows because new row data available. - this.$scope.visibleRows = []; - - this.$scope.overrideRowPositioning = false; - + MCTTableController.prototype.setRows = function (newRows) { //Nothing to show because no columns visible - if (!this.$scope.displayHeaders) { + if (!this.$scope.displayHeaders || !newRows) { return; } - this.filterAndSort(newRows || []); - this.resize(); + this.$scope.displayRows = this.filterAndSort(newRows || []); + this.resize(newRows).then(this.setVisibleRows.bind(this)); }; /** diff --git a/platform/features/table/src/controllers/RTTelemetryTableController.js b/platform/features/table/src/controllers/RealtimeTableController.js similarity index 59% rename from platform/features/table/src/controllers/RTTelemetryTableController.js rename to platform/features/table/src/controllers/RealtimeTableController.js index 8a61d61b5e..3f983207bc 100644 --- a/platform/features/table/src/controllers/RTTelemetryTableController.js +++ b/platform/features/table/src/controllers/RealtimeTableController.js @@ -37,7 +37,7 @@ define( * @param telemetryFormatter * @constructor */ - function RTTelemetryTableController($scope, telemetryHandler, telemetryFormatter) { + function RealtimeTableController($scope, telemetryHandler, telemetryFormatter) { TableController.call(this, $scope, telemetryHandler, telemetryFormatter); $scope.autoScroll = false; @@ -66,58 +66,35 @@ define( }); } - RTTelemetryTableController.prototype = Object.create(TableController.prototype); + RealtimeTableController.prototype = Object.create(TableController.prototype); /** - Override the subscribe function defined on the parent controller in - order to handle realtime telemetry instead of historical. + * Overrides method on TelemetryTableController providing handling + * for realtime data. */ - RTTelemetryTableController.prototype.subscribe = function () { - var self = this; - self.$scope.rows = undefined; - (this.subscriptions || []).forEach(function (unsubscribe){ - unsubscribe(); - }); + RealtimeTableController.prototype.addRealtimeData = function() { + var self = this, + datum, + row; + this.handle.getTelemetryObjects().forEach(function (telemetryObject){ + datum = self.handle.getDatum(telemetryObject); + if (datum) { + //Populate row values from telemetry datum + row = self.table.getRowValues(telemetryObject, datum); + self.$scope.rows.push(row); - if (this.handle) { - this.handle.unsubscribe(); - } - - function updateData(){ - var datum, - row; - self.handle.getTelemetryObjects().forEach(function (telemetryObject){ - datum = self.handle.getDatum(telemetryObject); - if (datum) { - row = self.table.getRowValues(telemetryObject, datum); - if (!self.$scope.rows){ - self.$scope.rows = [row]; - self.$scope.$digest(); - } else { - self.$scope.rows.push(row); - - if (self.$scope.rows.length > self.maxRows) { - self.$scope.$broadcast('remove:row', 0); - self.$scope.rows.shift(); - } - - self.$scope.$broadcast('add:row', - self.$scope.rows.length - 1); - } + //Inform table that a new row has been added + if (self.$scope.rows.length > self.maxRows) { + self.$scope.$broadcast('remove:row', 0); + self.$scope.rows.shift(); } - }); - } - - this.handle = this.$scope.domainObject && this.telemetryHandler.handle( - this.$scope.domainObject, - updateData, - true // Lossless - ); - - this.setup(); + self.$scope.$broadcast('add:row', + self.$scope.rows.length - 1); + } + }); }; - return RTTelemetryTableController; + return RealtimeTableController; } ); diff --git a/platform/features/table/src/controllers/TableOptionsController.js b/platform/features/table/src/controllers/TableOptionsController.js index c3b479073c..a28411e7d0 100644 --- a/platform/features/table/src/controllers/TableOptionsController.js +++ b/platform/features/table/src/controllers/TableOptionsController.js @@ -51,13 +51,29 @@ define( this.$scope = $scope; this.domainObject = $scope.domainObject; + this.listeners = []; $scope.columnsForm = {}; - this.domainObject.getCapability('mutation').listen(function (model) { - self.populateForm(model); + function unlisten() { + self.listeners.forEach(function (listener) { + listener(); + }); + } + + $scope.$watch('domainObject', function(domainObject) { + unlisten(); + self.populateForm(domainObject.getModel()); + + self.listeners.push(self.domainObject.getCapability('mutation').listen(function (model) { + self.populateForm(model); + })); }); + /** + * Maintain a configuration object on scope that stores column + * configuration. On change, synchronize with object model. + */ $scope.$watchCollection('configuration.table.columns', function (columns){ if (columns){ self.domainObject.useCapability('mutation', function (model) { @@ -67,6 +83,11 @@ define( } }); + /** + * Destroy all mutation listeners + */ + $scope.$on('$destroy', unlisten); + } TableOptionsController.prototype.populateForm = function (model) { @@ -86,7 +107,7 @@ define( 'key': key }); }); - this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration)); + this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration || {})); }; return TableOptionsController; diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index e579c5eeb8..36f54b1ac6 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -52,19 +52,15 @@ define( this.$scope = $scope; this.columns = {}; //Range and Domain columns this.handle = undefined; - //this.pending = false; this.telemetryHandler = telemetryHandler; this.table = new TableConfiguration($scope.domainObject, telemetryFormatter); this.changeListeners = []; - $scope.rows = undefined; + $scope.rows = []; // Subscribe to telemetry when a domain object becomes available - this.$scope.$watch('domainObject', function(domainObject){ - if (!domainObject) - return; - + this.$scope.$watch('domainObject', function(){ self.subscribe(); self.registerChangeListeners(); }); @@ -73,16 +69,24 @@ define( this.$scope.$on("$destroy", this.destroy.bind(this)); } + /** + * @private + */ + TelemetryTableController.prototype.unregisterChangeListeners = function () { + this.changeListeners.forEach(function (listener) { + return listener && listener(); + }); + this.changeListeners = []; + }; + /** * Defer registration of change listeners until domain object is * available in order to avoid race conditions * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { - this.changeListeners.forEach(function (listener) { - return listener && listener(); - }); - this.changeListeners = []; + this.unregisterChangeListeners(); + // When composition changes, re-subscribe to the various // telemetry subscriptions this.changeListeners.push(this.$scope.$watchCollection( @@ -103,25 +107,37 @@ define( } }; + /** + * Function for handling realtime data when it is available. This + * will be called by the telemetry framework when new data is + * available. + * + * Method should be overridden by specializing class. + */ + TelemetryTableController.prototype.addRealtimeData = function () { + }; + + /** + * Function for handling historical data. Will be called by + * telemetry framework when requested historical data is available. + * Should be overridden by specializing class. + */ + TelemetryTableController.prototype.addHistoricalData = function () { + }; + /** Create a new subscription. This can be overridden by children to change default behaviour (which is to retrieve historical telemetry only). */ TelemetryTableController.prototype.subscribe = function () { - var self = this; - if (this.handle) { this.handle.unsubscribe(); } - //Noop because not supporting realtime data right now - function noop(){ - } - this.handle = this.$scope.domainObject && this.telemetryHandler.handle( this.$scope.domainObject, - noop, + this.addRealtimeData.bind(this), true // Lossless ); @@ -130,28 +146,6 @@ define( this.setup(); }; - /** - * Populates historical data on scope when it becomes available - * @private - */ - TelemetryTableController.prototype.addHistoricalData = function () { - var rowData = [], - self = this; - - this.handle.getTelemetryObjects().forEach(function (telemetryObject){ - var series = self.handle.getSeries(telemetryObject) || {}, - pointCount = series.getPointCount ? series.getPointCount() : 0, - i = 0; - - for (; i < pointCount; i++) { - rowData.push(self.table.getRowValues(telemetryObject, - self.handle.makeDatum(telemetryObject, series, i))); - } - }); - - this.$scope.rows = rowData; - }; - /** * Setup table columns based on domain object metadata */ @@ -162,7 +156,9 @@ define( if (handle) { handle.promiseTelemetryObjects().then(function () { - table.buildColumns(handle.getMetadata()); + self.$scope.headers = []; + self.$scope.rows = []; + table.populateColumns(handle.getMetadata()); self.filterColumns(); @@ -176,26 +172,14 @@ define( } }; - /** - * @private - * @param object The object for which data is available (table may - * be composed of multiple objects) - * @param datum The data received from the telemetry source - */ - TelemetryTableController.prototype.updateRows = function (object, datum) { - this.$scope.rows.push(this.table.getRowValues(object, datum)); - }; - /** * When column configuration changes, update the visible headers * accordingly. * @private */ - TelemetryTableController.prototype.filterColumns = function (columnConfig) { - if (!columnConfig){ - columnConfig = this.table.getColumnConfiguration(); - this.table.saveColumnConfiguration(columnConfig); - } + TelemetryTableController.prototype.filterColumns = function () { + var columnConfig = this.table.buildColumnConfiguration(); + //Populate headers with visible columns (determined by configuration) this.$scope.headers = Object.keys(columnConfig).filter(function (column) { return columnConfig[column]; diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js index 575e830395..2d61669a2e 100644 --- a/platform/features/table/src/directives/MCTTable.js +++ b/platform/features/table/src/directives/MCTTable.js @@ -12,6 +12,51 @@ define( * Defines a generic 'Table' component. The table can be populated * en-masse by setting the rows attribute, or rows can be added as * needed via a broadcast 'addRow' event. + * + * This directive accepts parameters specifying header and row + * content, as well as some additional options. + * + * Two broadcast events for notifying the table that the rows have + * changed. For performance reasons, the table does not monitor the + * content of `rows` constantly. + * - 'add:row': A $broadcast event that will notify the table that + * a new row has been added to the table. + * eg. + *

+         * $scope.rows.push(newRow);
+         * $scope.$broadcast('add:row', $scope.rows.length-1);
+         * 
+ * The code above adds a new row, and alerts the table using the + * add:row event. Sorting and filtering will be applied + * automatically by the table component. + * + * - 'remove:row': A $broadcast event that will notify the table that a + * row should be removed from the table. + * eg. + *

+         * $scope.rows.slice(5, 1);
+         * $scope.$broadcast('remove:row', 5);
+         * 
+ * The code above removes a row from the rows array, and then alerts + * the table to its removal. + * + * @memberof platform/features/table + * @param {string[]} headers The column titles to appear at the top + * of the table. Corresponding values are specified in the rows + * using the header title provided here. + * @param {Object[]} rows The row content. Each row is an object + * with key-value pairs where the key corresponds to a header + * specified in the headers parameter. + * @param {boolean} enableFilter If true, values will be searchable + * and results filtered + * @param {boolean} enableSort If true, sorting will be enabled + * allowing sorting by clicking on column headers + * @param {boolean} autoScroll If true, table will automatically + * scroll to the bottom as new data arrives. Auto-scroll can be + * disengaged manually by scrolling away from the bottom of the + * table, and can also be enabled manually by scrolling to the bottom of + * the table rows. + * * @constructor */ function MCTTable($timeout) { diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js index 86a18aee5a..bfc6f50edb 100644 --- a/platform/features/table/test/TableConfigurationSpec.js +++ b/platform/features/table/test/TableConfigurationSpec.js @@ -116,10 +116,10 @@ define( }]; beforeEach(function() { - table.buildColumns(metadata); + table.populateColumns(metadata); }); - it("populates the columns attribute", function() { + it("populates columns", function() { expect(table.columns.length).toBe(5); }); @@ -141,7 +141,7 @@ define( it("Provides a default configuration with all columns" + " visible", function() { - var configuration = table.getColumnConfiguration(); + var configuration = table.buildColumnConfiguration(); expect(configuration).toBeDefined(); expect(Object.keys(configuration).every(function(key){ @@ -160,7 +160,7 @@ define( }; mockModel.configuration = modelConfig; - tableConfig = table.getColumnConfiguration(); + tableConfig = table.buildColumnConfiguration(); expect(tableConfig).toBeDefined(); expect(tableConfig['Range 1']).toBe(false); diff --git a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js b/platform/features/table/test/controllers/HistoricalTableControllerSpec.js similarity index 94% rename from platform/features/table/test/controllers/TelemetryTableControllerSpec.js rename to platform/features/table/test/controllers/HistoricalTableControllerSpec.js index 03f62f11e3..f000529467 100644 --- a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js +++ b/platform/features/table/test/controllers/HistoricalTableControllerSpec.js @@ -23,7 +23,7 @@ define( [ - "../../src/controllers/TelemetryTableController" + "../../src/controllers/HistoricalTableController" ], function (TableController) { "use strict"; @@ -73,14 +73,14 @@ define( mockTable = jasmine.createSpyObj('table', [ - 'buildColumns', - 'getColumnConfiguration', + 'populateColumns', + 'buildColumnConfiguration', 'getRowValues', 'saveColumnConfiguration' ] ); mockTable.columns = []; - mockTable.getColumnConfiguration.andReturn(mockConfiguration); + mockTable.buildColumnConfiguration.andReturn(mockConfiguration); mockDomainObject= jasmine.createSpyObj('domainObject', [ 'getCapability', @@ -126,21 +126,18 @@ define( expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled(); }); - describe('the controller makes use of the table', function () { + describe('makes use of the table', function () { it('to create column definitions from telemetry' + ' metadata', function () { controller.setup(); - expect(mockTable.buildColumns).toHaveBeenCalled(); + expect(mockTable.populateColumns).toHaveBeenCalled(); }); it('to create column configuration, which is written to the' + ' object model', function () { - var mockModel = {}; - controller.setup(); - expect(mockTable.getColumnConfiguration).toHaveBeenCalled(); - expect(mockTable.saveColumnConfiguration).toHaveBeenCalled(); + expect(mockTable.buildColumnConfiguration).toHaveBeenCalled(); }); }); diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 5e38c7e651..175d8fed0a 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -58,15 +58,18 @@ define( mockElement = jasmine.createSpyObj('element', [ 'find', + 'prop', 'on' ]); mockElement.find.andReturn(mockElement); + mockElement.prop.andReturn(0); mockScope.displayHeaders = true; mockTimeout = jasmine.createSpy('$timeout'); mockTimeout.andReturn(promise(undefined)); controller = new MCTTableController(mockScope, mockTimeout, mockElement); + spyOn(controller, 'setVisibleRows'); }); it('Reacts to changes to filters, headers, and rows', function() { @@ -115,7 +118,7 @@ define( }); it('Sets rows on scope when rows change', function() { - controller.updateRows(testRows); + controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); expect(mockScope.displayRows).toEqual(testRows); }); @@ -127,7 +130,7 @@ define( 'col2': {'text': 'ghi'}, 'col3': {'text': 'row3 col3'} }; - controller.updateRows(testRows); + controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); testRows.push(row4); addRowFunc(undefined, 3); @@ -136,10 +139,8 @@ define( it('Supports removing rows individually', function() { var removeRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-1].args[1]; - controller.updateRows(testRows); + controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); - spyOn(controller, 'setVisibleRows'); - //controller.setVisibleRows.andReturn(undefined); removeRowFunc(undefined, 2); expect(mockScope.displayRows.length).toBe(2); expect(controller.setVisibleRows).toHaveBeenCalled(); @@ -179,7 +180,54 @@ define( expect(sortedRows[2].col2.text).toEqual('abc'); }); - describe('Adding new rows', function() { + it('correctly sorts rows of differing types', function () { + mockScope.sortColumn = 'col2'; + mockScope.sortDirection = 'desc'; + + testRows.push({ + 'col1': {'text': 'row4 col1'}, + 'col2': {'text': '123'}, + 'col3': {'text': 'row4 col3'} + }); + testRows.push({ + 'col1': {'text': 'row5 col1'}, + 'col2': {'text': '456'}, + 'col3': {'text': 'row5 col3'} + }); + testRows.push({ + 'col1': {'text': 'row5 col1'}, + 'col2': {'text': ''}, + 'col3': {'text': 'row5 col3'} + }); + + sortedRows = controller.sortRows(testRows); + expect(sortedRows[0].col2.text).toEqual('ghi'); + expect(sortedRows[1].col2.text).toEqual('def'); + expect(sortedRows[2].col2.text).toEqual('abc'); + + expect(sortedRows[sortedRows.length-3].col2.text).toEqual('456'); + expect(sortedRows[sortedRows.length-2].col2.text).toEqual('123'); + expect(sortedRows[sortedRows.length-1].col2.text).toEqual(''); + }); + + describe('The sort comparator', function () { + it('Correctly sorts different data types', function () { + var val1 = "", + val2 = "1", + val3 = "2016-04-05 18:41:30.713Z", + val4 = "1.1", + val5 = "8.945520958175627e-13"; + mockScope.sortDirection = "asc"; + + expect(controller.sortComparator(val1, val2)).toEqual(-1); + expect(controller.sortComparator(val3, val1)).toEqual(1); + expect(controller.sortComparator(val3, val2)).toEqual(1); + expect(controller.sortComparator(val4, val2)).toEqual(1); + expect(controller.sortComparator(val2, val5)).toEqual(1); + }); + }); + + describe('Adding new rows', function () { var row4, row5, row6; @@ -210,20 +258,20 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); mockScope.rows.push(row4); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); mockScope.rows.push(row5); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[4].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); //Add a duplicate row mockScope.rows.push(row6); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); }); @@ -239,18 +287,18 @@ define( mockScope.displayRows = controller.filterRows(testRows); mockScope.rows.push(row5); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows[1].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows.length).toBe(2); //Row was not added because does not match filter }); it('Adds new rows at the correct sort position when' + - ' not sorted ', function() { + ' not sorted ', function () { mockScope.sortColumn = undefined; mockScope.sortDirection = undefined; mockScope.filters = {}; @@ -258,14 +306,33 @@ define( mockScope.displayRows = testRows.slice(0); mockScope.rows.push(row5); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[3].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.newRow(undefined, mockScope.rows.length-1); + controller.addRow(undefined, mockScope.rows.length-1); expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); }); + it('Resizes columns if length of any columns in new' + + ' row exceeds corresponding existing column', function() { + var row7 = { + 'col1': {'text': 'row6 col1'}, + 'col2': {'text': 'some longer string'}, + 'col3': {'text': 'row6 col3'} + }; + + mockScope.sortColumn = undefined; + mockScope.sortDirection = undefined; + mockScope.filters = {}; + + mockScope.displayRows = testRows.slice(0); + + mockScope.rows.push(row7); + controller.addRow(undefined, mockScope.rows.length-1); + expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); + }); + }); }); diff --git a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js b/platform/features/table/test/controllers/RealtimeTableControllerSpec.js similarity index 94% rename from platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js rename to platform/features/table/test/controllers/RealtimeTableControllerSpec.js index 59911d1771..e0b6978d88 100644 --- a/platform/features/table/test/controllers/RTTelemetryTableControllerSpec.js +++ b/platform/features/table/test/controllers/RealtimeTableControllerSpec.js @@ -23,7 +23,7 @@ define( [ - "../../src/controllers/RTTelemetryTableController" + "../../src/controllers/RealtimeTableController" ], function (TableController) { "use strict"; @@ -77,14 +77,14 @@ define( mockTable = jasmine.createSpyObj('table', [ - 'buildColumns', - 'getColumnConfiguration', + 'populateColumns', + 'buildColumnConfiguration', 'getRowValues', 'saveColumnConfiguration' ] ); mockTable.columns = []; - mockTable.getColumnConfiguration.andReturn(mockConfiguration); + mockTable.buildColumnConfiguration.andReturn(mockConfiguration); mockTable.getRowValues.andReturn(mockTableRow); mockDomainObject= jasmine.createSpyObj('domainObject', [ @@ -107,13 +107,16 @@ define( 'unsubscribe', 'getDatum', 'promiseTelemetryObjects', - 'getTelemetryObjects' + 'getTelemetryObjects', + 'request' ]); + // Arbitrary array with non-zero length, contents are not // used by mocks mockTelemetryHandle.getTelemetryObjects.andReturn([{}]); mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); mockTelemetryHandle.getDatum.andReturn({}); + mockTelemetryHandle.request.andReturn(promise(undefined)); mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ 'handle' diff --git a/platform/features/table/test/controllers/TableOptionsControllerSpec.js b/platform/features/table/test/controllers/TableOptionsControllerSpec.js index 9de96b5f52..fd6d8b43fe 100644 --- a/platform/features/table/test/controllers/TableOptionsControllerSpec.js +++ b/platform/features/table/test/controllers/TableOptionsControllerSpec.js @@ -47,18 +47,36 @@ define( 'listen' ]); mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability' + 'getCapability', + 'getModel' ]); mockDomainObject.getCapability.andReturn(mockCapability); + mockDomainObject.getModel.andReturn({}); + mockScope = jasmine.createSpyObj('scope', [ - '$watchCollection' + '$watchCollection', + '$watch', + '$on' ]); mockScope.domainObject = mockDomainObject; controller = new TableOptionsController(mockScope); }); + it('Listens for changing domain object', function() { + expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function)); + }); + + it('On destruction of controller, destroys listeners', function() { + var unlistenFunc = jasmine.createSpy("unlisten"); + controller.listeners.push(unlistenFunc); + expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function)); + mockScope.$on.mostRecentCall.args[1](); + expect(unlistenFunc).toHaveBeenCalled(); + }); + it('Registers a listener for mutation events on the object', function() { + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); expect(mockCapability.listen).toHaveBeenCalled(); }); From d385e55e76bb955b40c81977d0b879dfa10a47c1 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 22 Apr 2016 10:46:07 -0700 Subject: [PATCH 27/38] [Build] Remove snapshot suffix Closes sprint Heinlein, https://github.com/nasa/openmct/milestones/Heinlein --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6830d197e6..183c92c7da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openmctweb", - "version": "0.10.0-SNAPSHOT", + "version": "0.10.0", "description": "The Open MCT core platform", "dependencies": { "express": "^4.13.1", From 0ccb696ee204efeb3244ad583328bb7767341693 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 22 Apr 2016 10:51:12 -0700 Subject: [PATCH 28/38] [Build] Bump version number, add -SNAPSHOT ...to begin sprint Herbert, https://github.com/nasa/openmct/milestones/Herbert --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 183c92c7da..f7854dbba2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openmctweb", - "version": "0.10.0", + "version": "0.10.1-SNAPSHOT", "description": "The Open MCT core platform", "dependencies": { "express": "^4.13.1", From a13d0b7fab9a4cea0bc91d8759f14d0ef39847d4 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 26 Apr 2016 15:52:09 -0700 Subject: [PATCH 29/38] [Examples] Move examples out of main.js ...such that they are not compiled into the Open MCT that is used as the basis for non-example applications. Addresses #835 --- index.html | 6 +++++- main.js | 8 ++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index e5145d5c5e..e16b096abb 100644 --- a/index.html +++ b/index.html @@ -30,7 +30,11 @@ diff --git a/main.js b/main.js index 33e45c0125..f3d0fb07cf 100644 --- a/main.js +++ b/main.js @@ -89,11 +89,7 @@ define([ './platform/entanglement/bundle', './platform/search/bundle', './platform/status/bundle', - './platform/commonUI/regions/bundle', - - './example/imagery/bundle', - './example/eventGenerator/bundle', - './example/generator/bundle' + './platform/commonUI/regions/bundle' ], function (Main, legacyRegistry) { 'use strict'; @@ -103,4 +99,4 @@ define([ return new Main().run(legacyRegistry); } }; -}); \ No newline at end of file +}); From ff36d9ee8064d28cb48a4d5d7608865768270eb3 Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Fri, 29 Apr 2016 10:50:24 -0700 Subject: [PATCH 30/38] [Frontend] Removed bullets from ol, ul open #869 - This should be cherry-picked into main branches as well. (cherry picked from commit 4c97413) --- platform/commonUI/general/res/sass/_global.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/platform/commonUI/general/res/sass/_global.scss b/platform/commonUI/general/res/sass/_global.scss index 27c08551df..be2f7014c1 100644 --- a/platform/commonUI/general/res/sass/_global.scss +++ b/platform/commonUI/general/res/sass/_global.scss @@ -84,7 +84,11 @@ p { margin-bottom: $interiorMarginLg; } -ol, ul { padding-left: 0; } +ol, ul { + list-style: none; + margin: 0; + padding-left: 0; +} mct-container { display: block; From de2703ee48a4bbfb7b0864a954ea277239c8df8f Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 4 May 2016 10:08:55 -0700 Subject: [PATCH 31/38] [Branding] Remove Web from name at top-level Per #816, change Open MCT Web to Open MCT in all top-level files. --- CONTRIBUTING.md | 18 +++++++++--------- README.md | 22 +++++++++++----------- bower.json | 6 +++--- package.json | 4 ++-- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e726007800..29dbbc46cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to Open MCT Web +# Contributing to Open MCT -This document describes the process of contributing to Open MCT Web as well +This document describes the process of contributing to Open MCT as well as the standards that will be applied when evaluating contributions. Please be aware that additional agreements will be necessary before we can @@ -21,9 +21,9 @@ The short version: ## Contribution Process -Open MCT Web uses git for software version control, and for branching and +Open MCT uses git for software version control, and for branching and merging. The central repository is at -https://github.com/nasa/openmctweb.git. +https://github.com/nasa/openmct.git. ### Roles @@ -116,18 +116,18 @@ the merge back to the master branch. ## Standards -Contributions to Open MCT Web are expected to meet the following standards. +Contributions to Open MCT are expected to meet the following standards. In addition, reviewers should use general discretion before accepting changes. ### Code Standards -JavaScript sources in Open MCT Web must satisfy JSLint under its default +JavaScript sources in Open MCT must satisfy JSLint under its default settings. This is verified by the command line build. #### Code Guidelines -JavaScript sources in Open MCT Web should: +JavaScript sources in Open MCT should: * Use four spaces for indentation. Tabs should not be used. * Include JSDoc for any exposed API (e.g. public methods, constructors.) @@ -159,7 +159,7 @@ JavaScript sources in Open MCT Web should: * Third, imperative statements. * Finally, the returned value. -Deviations from Open MCT Web code style guidelines require two-party agreement, +Deviations from Open MCT code style guidelines require two-party agreement, typically from the author of the change and its reviewer. #### Code Example @@ -260,7 +260,7 @@ these standards. ## Issue Reporting -Issues are tracked at https://github.com/nasa/openmctweb/issues +Issues are tracked at https://github.com/nasa/openmct/issues Issues should include: diff --git a/README.md b/README.md index ed488a0e5c..dc18671dd3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Open MCT Web +# Open MCT -Open MCT Web is a web-based platform for mission operations user interface +Open MCT is a web-based platform for mission operations user interface software. ## Bundles @@ -8,7 +8,7 @@ software. A bundle is a group of software components (including source code, declared as AMD modules, as well as resources such as images and HTML templates) that are intended to be added or removed as a single unit. A plug-in for -Open MCT Web will be expressed as a bundle; platform components are also +Open MCT will be expressed as a bundle; platform components are also expressed as bundles. A bundle is also just a directory which contains a file `bundle.json`, @@ -16,7 +16,7 @@ which declares its contents. The file `bundles.json` (note the plural), at the top level of the repository, is a JSON file containing an array of all bundles (expressed as -directory names) to include in a running instance of Open MCT Web. Adding or +directory names) to include in a running instance of Open MCT. Adding or removing paths from this list will add or remove bundles from the running application. @@ -56,7 +56,7 @@ To run: ## Build -Open MCT Web is built using [`npm`](http://npmjs.com/) +Open MCT is built using [`npm`](http://npmjs.com/) and [`gulp`](http://gulpjs.com/). To build: @@ -64,18 +64,18 @@ To build: `npm run prepublish` This will compile and minify JavaScript sources, as well as copy over assets. -The contents of the `dist` folder will contain a runnable Open MCT Web +The contents of the `dist` folder will contain a runnable Open MCT instance (e.g. by starting an HTTP server in that directory), including: -* A `main.js` file containing Open MCT Web source code. +* A `main.js` file containing Open MCT source code. * Various assets in the `example` and `platform` directories. -* An `index.html` that runs Open MCT Web in its default configuration. +* An `index.html` that runs Open MCT in its default configuration. Additional `gulp` tasks are defined in [the gulpfile](gulpfile.js). ### Building Documentation -Open MCT Web's documentation is generated by an +Open MCT's documentation is generated by an [npm](https://www.npmjs.com/)-based build. It has additional dependencies that may not be available on every platform and thus is not covered in the standard npm install. Ensure your system has [libcairo](http://cairographics.org/) @@ -89,7 +89,7 @@ Documentation will be generated in `target/docs`. # Glossary -Certain terms are used throughout Open MCT Web with consistent meanings +Certain terms are used throughout Open MCT with consistent meanings or conventions. Any deviations from the below are issues and should be addressed (either by updating this glossary or changing code to reflect correct usage.) Other developer documentation, particularly in-line @@ -112,7 +112,7 @@ documentation, may presume an understanding of these terms. (Most often used in the context of extensions, domain object models, or other similar application-specific objects.) * _domain object_: A meaningful object to the user; a distinct thing in - the work support by Open MCT Web. Anything that appears in the left-hand + the work support by Open MCT. Anything that appears in the left-hand tree is a domain object. * _extension_: An extension is a unit of functionality exposed to the platform in a declarative fashion by a bundle. For more diff --git a/bower.json b/bower.json index 7285aad9e2..7c913754cf 100644 --- a/bower.json +++ b/bower.json @@ -1,10 +1,10 @@ { - "name": "openmctweb", - "description": "The OpenMCTWeb core platform", + "name": "openmct", + "description": "The Open MCT core platform", "main": "", "license": "Apache-2.0", "moduleType": [], - "homepage": "http://nasa.github.io/openmctweb/", + "homepage": "http://nasa.github.io/openmct/", "private": true, "dependencies": { "angular": "1.4.4", diff --git a/package.json b/package.json index f7854dbba2..9665703f11 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "openmctweb", + "name": "openmct", "version": "0.10.1-SNAPSHOT", "description": "The Open MCT core platform", "dependencies": { @@ -53,7 +53,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/nasa/openmctweb.git" + "url": "https://github.com/nasa/openmct.git" }, "author": "", "license": "Apache-2.0", From 431b8365687366388335cb40cc2fcee1e649d17e Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 4 May 2016 10:13:09 -0700 Subject: [PATCH 32/38] [Branding] Change name in docs hierarchy ...from Open MCT Web to plain-old Open MCT. --- docs/src/architecture/framework.md | 16 +- docs/src/architecture/index.md | 24 +-- docs/src/architecture/platform.md | 40 ++--- docs/src/guide/index.md | 204 ++++++++++++------------- docs/src/index.md | 13 +- docs/src/process/cycle.md | 2 +- docs/src/process/index.md | 4 +- docs/src/process/testing/plan.md | 2 +- docs/src/process/testing/procedures.md | 6 +- docs/src/tutorials/index.md | 142 ++++++++--------- 10 files changed, 226 insertions(+), 227 deletions(-) diff --git a/docs/src/architecture/framework.md b/docs/src/architecture/framework.md index 229bee39c4..e269e38f61 100644 --- a/docs/src/architecture/framework.md +++ b/docs/src/architecture/framework.md @@ -5,7 +5,7 @@ software components to communicate. The software components it recognizes are: * _Extensions_: Individual units of functionality that can be added to - or removed from Open MCT Web. _Extension categories_ distinguish what + or removed from Open MCT. _Extension categories_ distinguish what type of functionality is being added/removed. * _Bundles_: A grouping of related extensions (named after an analogous concept from [OSGi](http://www.osgi.org/)) @@ -19,7 +19,7 @@ manner which the framework layer can understand. ```nomnoml #direction: down -[Open MCT Web| +[Open MCT| [Dependency injection framework]-->[Platform bundle #1] [Dependency injection framework]-->[Platform bundle #2] [Dependency injection framework]-->[Plugin bundle #1] @@ -35,7 +35,7 @@ manner which the framework layer can understand. ``` The "dependency injection framework" in this case is -[AngularJS](https://angularjs.org/). Open MCT Web's framework layer +[AngularJS](https://angularjs.org/). Open MCT's framework layer is really just a thin wrapper over Angular that recognizes the concepts of bundles and extensions (as declared in JSON files) and registering extensions with Angular. It additionally acts as a @@ -60,7 +60,7 @@ activities which were performed by the framework component. ## Application Initialization -The framework component initializes an Open MCT Web application following +The framework component initializes an Open MCT application following a simple sequence of steps. ```nomnoml @@ -97,7 +97,7 @@ a simple sequence of steps. [Extension]o->[Dependency #3] ``` -Open MCT Web's architecture relies on a simple premise: Individual units +Open MCT's architecture relies on a simple premise: Individual units (extensions) only have access to the dependencies they declare that they need, and they acquire references to these dependencies via dependency injection. This has several desirable traits: @@ -121,11 +121,11 @@ injection. This has several desirable traits: the framework. A drawback to this approach is that it makes it difficult to define -"the architecture" of Open MCT Web, in terms of describing the specific +"the architecture" of Open MCT, in terms of describing the specific units that interact at run-time. The run-time architecture is determined by the framework as the consequence of wiring together dependencies. As such, the specific architecture of any given application built on -Open MCT Web can look very different. +Open MCT can look very different. Keeping that in mind, there are a few useful patterns supported by the framework that are useful to keep in mind. @@ -229,4 +229,4 @@ otherwise a single provider) will be exposed as a single service that other extensions can acquire through dependency injection. Because all components of the same type of service expose the same interface, users of that service do not need to be aware that they are talking to an -aggregator or a provider, for instance. \ No newline at end of file +aggregator or a provider, for instance. diff --git a/docs/src/architecture/index.md b/docs/src/architecture/index.md index 7354402d0c..a4586a3542 100644 --- a/docs/src/architecture/index.md +++ b/docs/src/architecture/index.md @@ -1,14 +1,14 @@ # Introduction The purpose of this document is to familiarize developers with the -overall architecture of Open MCT Web. +overall architecture of Open MCT. The target audience includes: * _Platform maintainers_: Individuals involved in developing, extending, and maintaing capabilities of the platform. * _Integration developers_: Individuals tasked with integrated - Open MCT Web into a larger system, who need to understand + Open MCT into a larger system, who need to understand its inner workings sufficiently to complete this integration. As the focus of this document is on architecture, whenever possible @@ -17,25 +17,25 @@ omitted. These details may be found in the developer guide. # Overview -Open MCT Web is client software: It runs in a web browser and +Open MCT is client software: It runs in a web browser and provides a user interface, while communicating with various server-side resources through browser APIs. ```nomnoml #direction: right -[Client|[Browser|[Open MCT Web]->[Browser APIs]]] +[Client|[Browser|[Open MCT]->[Browser APIs]]] [Server|[Web services]] [Client]<->[Server] ``` -While Open MCT Web can be configured to run as a standalone client, +While Open MCT can be configured to run as a standalone client, this is rarely very useful. Instead, it is intended to be used as a display and interaction layer for information obtained from a variety of back-end services. Doing so requires authoring or utilizing -adapter plugins which allow Open MCT Web to interact with these services. +adapter plugins which allow Open MCT to interact with these services. Typically, the pattern here is to provide a known interface that -Open MCT Web can utilize, and implement it such that it interacts with +Open MCT can utilize, and implement it such that it interacts with whatever back-end provides the relevant information. Examples of back-ends that can be utilized in this fashion include databases for the persistence of user-created objects, or sources of @@ -43,13 +43,13 @@ telemetry data. ## Software Architecture -The simplest overview of Open MCT Web is to look at it as a "layered" +The simplest overview of Open MCT is to look at it as a "layered" architecture, where each layer more clearly specifies the behavior of the software. ```nomnoml #direction: down -[Open MCT Web| +[Open MCT| [Platform]<->[Application] [Framework]->[Application] [Framework]->[Platform] @@ -64,14 +64,14 @@ These layers are: established an abstraction by which different software components may communicate and/or interact. * [_Platform_](platform.md): The platform layer defines the general look, - feel, and behavior of Open MCT Web. This includes user-facing components like + feel, and behavior of Open MCT. This includes user-facing components like Browse mode and Edit mode, as well as underlying elements of the information model and the general service infrastructure. * _Application_: The application layer defines specific features of - an application built on Open MCT Web. This includes adapters to + an application built on Open MCT. This includes adapters to specific back-ends, new types of things for users to create, and new ways of visualizing objects within the system. This layer - typically consists of a mix of custom plug-ins to Open MCT Web, + typically consists of a mix of custom plug-ins to Open MCT, as well as optional features (such as Plot view) included alongside the platform. diff --git a/docs/src/architecture/platform.md b/docs/src/architecture/platform.md index a59a6ebf9c..1f5e087a11 100644 --- a/docs/src/architecture/platform.md +++ b/docs/src/architecture/platform.md @@ -1,6 +1,6 @@ # Overview -The Open MCT Web platform utilizes the [framework layer](Framework.md) +The Open MCT platform utilizes the [framework layer](Framework.md) to provide an extensible baseline for applications which includes: * A common user interface (and user interface paradigm) for dealing with @@ -16,7 +16,7 @@ building application, the platform adds more specificity by defining additional extension types and allowing for integration with back end components. -The run-time architecture of an Open MCT Web application can be categorized +The run-time architecture of an Open MCT application can be categorized into certain high-level tiers: ```nomnoml @@ -29,7 +29,7 @@ into certain high-level tiers: [Browser APIs]->[Back-end] ``` -Applications built using Open MCT Web may add or configure functionality +Applications built using Open MCT may add or configure functionality in __any of these tiers__. * _DOM_: The rendered HTML document, composed from HTML templates which @@ -60,7 +60,7 @@ in __any of these tiers__. functionality needed to support the information model. This includes exposing underlying sets of extensions and mediating with the back-end. -* _Back-end_: The back-end is out of the scope of Open MCT Web, except +* _Back-end_: The back-end is out of the scope of Open MCT, except for the interfaces which are utilized by adapters participating in the service infrastructure. Includes the underlying persistence stores, telemetry  streams, and so forth which the Open MCT Web client is being used to interact  @@ -70,15 +70,15 @@ in __any of these tiers__. Once the [application has been initialized](Framework.md#application-initialization) -Open MCT Web primarily operates in an event-driven paradigm; various +Open MCT primarily operates in an event-driven paradigm; various events (mouse clicks, timers firing, receiving responses to XHRs) trigger the invocation of functions, typically in the presentation layer for user actions or in the service infrastructure for server responses. -The "main point of entry" into an initialized Open MCT Web application +The "main point of entry" into an initialized Open MCT application is effectively the [route](https://docs.angularjs.org/api/ngRoute/service/$route#example) -which is associated with the URL used to access Open MCT Web (or a +which is associated with the URL used to access Open MCT (or a default route.) This route will be associated with a template which will be displayed; this template will include references to directives and controllers which will be interpreted by Angular and used to @@ -107,11 +107,11 @@ both the information model and the service infrastructure. # Presentation Layer -The presentation layer of Open MCT Web is responsible for providing +The presentation layer of Open MCT is responsible for providing information to display within templates, and for handling interactions which are initiated from templated DOM elements. AngularJS acts as an intermediary between the web page as the user sees it, and the -presentation layer implemented as Open MCT Web extensions. +presentation layer implemented as Open MCT extensions. ```nomnoml [Presentation Layer| @@ -143,12 +143,12 @@ to primitives from AngularJS: attributes and tags. * [_Routes_](https://docs.angularjs.org/api/ngRoute/service/$route#example) are used to associate specific URLs (including the fragment identifier) - with specific application states. (In Open MCT Web, these are used to + with specific application states. (In Open MCT, these are used to describe the mode of usage - e.g. browse or edit - as well as to identify the object being used.) * [_Templates_](https://docs.angularjs.org/guide/templates) are partial HTML documents that will be rendered and kept up-to-date by AngularJS. - Open MCT Web introduces a custom `mct-include` directive which acts + Open MCT introduces a custom `mct-include` directive which acts as a wrapper around `ng-include` to allow templates to be referred to by symbolic names. @@ -189,10 +189,10 @@ to displaying domain objects. ] ``` -Domain objects are the most fundamental component of Open MCT Web's +Domain objects are the most fundamental component of Open MCT's information model. A domain object is some distinct thing relevant to a user's work flow, such as a telemetry channel, display, or similar. -Open MCT Web is a tool for viewing, browsing, manipulating, and otherwise +Open MCT is a tool for viewing, browsing, manipulating, and otherwise interacting with a graph of domain objects. A domain object should be conceived of as the union of the following: @@ -254,7 +254,7 @@ Concrete examples of capabilities which follow this pattern # Service Infrastructure -Most services exposed by the Open MCT Web platform follow the +Most services exposed by the Open MCT platform follow the [composite services](Framework.md#composite-services) to permit a higher degree of flexibility in how a service can be modified or customized for specific applications. @@ -327,7 +327,7 @@ A short summary of the roles of these services: [DomainObjectProvider]o-[CapabilityService] ``` -As domain objects are central to Open MCT Web's information model, +As domain objects are central to Open MCT's information model, acquiring domain objects is equally important. ```nomnoml @@ -338,7 +338,7 @@ acquiring domain objects is equally important. [ Instantiate DomainObject]->[ End] ``` -Open MCT Web includes an implementation of an `ObjectService` which +Open MCT includes an implementation of an `ObjectService` which satisfies this capability by: * Consulting the [Model Service](#model-service) to acquire domain object @@ -437,9 +437,9 @@ objects (this allows failures to be recognized and handled in groups.) The telemetry service is responsible for acquiring telemetry data. Notably, the platform does not include any providers for -`TelemetryService`; applications built on Open MCT Web will need to +`TelemetryService`; applications built on Open MCT will need to implement a provider for this service if they wish to expose telemetry -data. This is usually the most important step for integrating Open MCT Web +data. This is usually the most important step for integrating Open MCT into an existing telemetry system. Requests for telemetry data are usually initiated in the @@ -721,6 +721,6 @@ disallow. ``` The type service provides metadata about the different types of domain -objects that exist within an Open MCT Web application. The platform +objects that exist within an Open MCT application. The platform implementation reads these types in from extension category `types` -and wraps them in a JavaScript interface. \ No newline at end of file +and wraps them in a JavaScript interface. diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index 7016462a5a..081f1df45a 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -1,4 +1,4 @@ -# Open MCT Web Developer Guide +# Open MCT Developer Guide Victor Woeltjen [victor.woeltjen@nasa.gov](mailto:victor.woeltjen@nasa.gov) @@ -18,24 +18,24 @@ April 5, 2016 | 1.2 | Added Mct-table directive | Andrew Henry The purpose of this guide is to familiarize software developers with the Open MCT Web platform. -## What is Open MCT Web -Open MCT Web is a platform for building user interface and display tools, +## What is Open MCT +Open MCT is a platform for building user interface and display tools, developed at the NASA Ames Research Center in collaboration with teams at the Jet Propulsion Laboratory. It is written in HTML5, CSS3, and JavaScript, using [AngularJS](http://www.angularjs.org) as a framework. Its intended use is to create single-page web applications which integrate data and behavior from a variety of sources and domains. -Open MCT Web has been developed to support the remote operation of space +Open MCT has been developed to support the remote operation of space vehicles, so some of its features are specific to that task; however, it is flexible enough to be adapted to a variety of other application domains where a display tool oriented toward browsing, composing, and visualizing would be useful. -Open MCT Web provides: +Open MCT provides: * A common user interface paradigm which can be applied to a variety of domains -and tasks. Open MCT Web is more than a widget toolkit - it provides a standard +and tasks. Open MCT is more than a widget toolkit - it provides a standard tree-on-the-left, view-on-the-right browsing environment which you customize by adding new browsable object types, visualizations, and back-end adapters. * A plugin framework and an extensible API for introducing new application @@ -44,17 +44,17 @@ features of a variety of types. visualizations and infrastructure specific to telemetry display. ## Client-Server Relationship -Open MCT Web is client software - it runs entirely in the user's web browser. As +Open MCT is client software - it runs entirely in the user's web browser. As such, it is largely 'server agnostic'; any web server capable of serving files -from paths is capable of providing Open MCT Web. +from paths is capable of providing Open MCT. -While Open MCT Web can be configured to run as a standalone client, this is +While Open MCT can be configured to run as a standalone client, this is rarely very useful. Instead, it is intended to be used as a display and interaction layer for information obtained from a variety of back-end services. Doing so requires authoring or utilizing adapter plugins which allow Open MCT Web to interact with these services. -Typically, the pattern here is to provide a known interface that Open MCT Web +Typically, the pattern here is to provide a known interface that Open MCT can utilize, and implement it such that it interacts with whatever back-end provides the relevant information. Examples of back-ends that can be utilized in this fashion include databases for the persistence of user-created objects, or @@ -63,52 +63,52 @@ sources of telemetry data. See the [Architecture Guide](../architecture/index.md#Overview) for information on the client-server relationship. -## Developing with Open MCT Web -Building applications with Open MCT Web typically means authoring and utilizing +## Developing with Open MCT +Building applications with Open MCT typically means authoring and utilizing a set of plugins which provide application-specific details about how Open MCT Web should behave. ### Technologies -Open MCT Web sources are written in JavaScript, with a number of configuration +Open MCT sources are written in JavaScript, with a number of configuration files written in JSON. Displayable components are written in HTML5 and CSS3. -Open MCT Web is built using [AngularJS](http://www.angularjs.org) from Google. A +Open MCT is built using [AngularJS](http://www.angularjs.org) from Google. A good understanding of Angular is recommended for developers working with Open MCT Web. ### Forking -Open MCT Web does not currently have a single stand-alone artifact that can be +Open MCT does not currently have a single stand-alone artifact that can be used as a library. Instead, the recommended approach for creating a new -application is to start by forking/branching Open MCT Web, and then adding new -features from there. Put another way, Open MCT Web's source structure is built +application is to start by forking/branching Open MCT, and then adding new +features from there. Put another way, Open MCT's source structure is built to serve as a template for specific applications. -Forking in this manner should not require that you edit Open MCT Web's sources. +Forking in this manner should not require that you edit Open MCT's sources. The preferred approach is to create a new directory (peer to `index.html`) for the new application, then add new bundles (as described in the Framework chapter) within that directory. -To initially clone the Open MCT Web repository: +To initially clone the Open MCT repository: `git clone -b open-master` -To create a fork to begin working on a new application using Open MCT Web: +To create a fork to begin working on a new application using Open MCT: cd git checkout open-master git checkout -b -As a convention used internally, applications built using Open MCT Web have +As a convention used internally, applications built using Open MCT have master branch names with an identifying prefix. For instance, if building an application called 'Foo', the last statement above would look like: git checkout -b foo-master -This convention is not enforced or understood by Open MCT Web in any way; it is +This convention is not enforced or understood by Open MCT in any way; it is mentioned here as a more general recommendation. # Overview -Open MCT Web is implemented as a framework component which manages a set of +Open MCT is implemented as a framework component which manages a set of other components. These components, called _bundles_, act as containers to group sets of related functionality; individual units of functionality are expressed within these bundles as _extensions_. @@ -119,7 +119,7 @@ run-time to satisfy these declared dependency. This dependency injection approach allows software components which have been authored separately (e.g. as plugins) but to collaborate at run-time. -Open MCT Web's framework layer is implemented on top of AngularJS's [dependency +Open MCT's framework layer is implemented on top of AngularJS's [dependency injection mechanism](https://docs.angularjs.org/guide/di) and is modelled after [OSGi](hhttp://www.osgi.org/) and its [Declarative Services component model](http://wiki.osgi.org/wiki/Declarative_Services). In particular, this is where the term _bundle_ comes from. @@ -134,7 +134,7 @@ The framework is described in more detail in the [Framework Overview](../archite architecture guide. ### Tiers -While all bundles in a running Open MCT Web instance are effectively peers, it +While all bundles in a running Open MCT instance are effectively peers, it is useful to think of them as a tiered architecture, where each tier adds more specificity to the application. ```nomnoml @@ -152,7 +152,7 @@ It additionally interprets bundle definitions (see explanation below, as well as further detail in the Framework chapter.) At this tier, we are at our most general: We know only that we are a plugin-based application. * __Platform__: Components in the Platform tier describe both the general user -interface and corresponding developer-facing interfaces of Open MCT Web. This +interface and corresponding developer-facing interfaces of Open MCT. This tier provides the general infrastructure for applications. It is less general than the framework tier, insofar as this tier introduces a specific user interface paradigm, but it is still non-specific as to what useful features @@ -160,7 +160,7 @@ will be provided. Although they can be removed or replaced easily, bundles provided by the Platform tier generally should not be thought of as optional. * __Application__: The application tier consists of components which utilize the infrastructure provided by the Platform to provide functionality which will (or -could) be useful to specific applications built using Open MCT Web. These +could) be useful to specific applications built using Open MCT. These include adapters to specific persistence back-ends (such as ElasticSearch or CouchDB) as well as bundles which describe more user-facing features (such as _Plot_ views for visualizing time series data, or _Layout_ objects for @@ -169,20 +169,20 @@ compromising basic application functionality, with the caveat that at least one persistence adapter needs to be present. * __Plugins__: Conceptually, this tier is not so different from the application tier; it consists of bundles describing new features, back-end adapters, that -are specific to the application being built on Open MCT Web. It is described as +are specific to the application being built on Open MCT. It is described as a separate tier here because it has one important distinction from the application tier: It consists of bundles that are not included with the platform (either authored anew for the specific application, or obtained from elsewhere.) Note that bundles in any tier can go off and consult back-end services. In practice, this responsibility is handled at the Application and/or Plugin tiers; -Open MCT Web is built to be server-agnostic, so any back-end is considered an +Open MCT is built to be server-agnostic, so any back-end is considered an application-specific detail. ## Platform Overview The "tiered" architecture described in the preceding text describes a way of -thinking of and categorizing software components of a Open MCT Web application, +thinking of and categorizing software components of a Open MCT application, as well as the framework layer's role in mediating between these components. Once the framework layer has wired these software components together, however, the application's logical architecture emerges. @@ -193,7 +193,7 @@ section of the Platform guide ### Web Services -As mentioned in the Introduction, Open MCT Web is a platform single-page +As mentioned in the Introduction, Open MCT is a platform single-page applications which runs entirely in the browser. Most applications will want to additionally interact with server-side resources, to (for example) read telemetry data or store user-created objects. This interaction is handled by @@ -206,7 +206,7 @@ individual bundles using APIs which are supported in browser (such as [Web Service #2] <- [Web Browser] [Web Service #3] <- [Web Browser] [ Web Browser | - [ Open MCT Web | + [ Open MCT | [Plugin Bundle #1]-->[Core API] [Core API]<--[Plugin Bundle #2] [Platform Bundle #1]-->[Core API] @@ -216,16 +216,16 @@ individual bundles using APIs which are supported in browser (such as [Core API]<--[Platform Bundle #5] [Core API]<--[Plugin Bundle #3] ] - [Open MCT Web] ->[Browser APIs] + [Open MCT] ->[Browser APIs] ] ``` This architectural approach ensures a loose coupling between applications built -using Open MCT Web and the backends which support them. +using Open MCT and the backends which support them. ### Glossary -Certain terms are used throughout Open MCT Web with consistent meanings or +Certain terms are used throughout Open MCT with consistent meanings or conventions. Other developer documentation, particularly in-line documentation, may presume an understanding of these terms. @@ -247,7 +247,7 @@ readable description of a thing; usually a single sentence or short paragraph. (Most often used in the context of extensions, domain object models, or other similar application-specific objects.) * __domain object__: A meaningful object to the user; a distinct thing in the -work support by Open MCT Web. Anything that appears in the left-hand tree is a +work support by Open MCT. Anything that appears in the left-hand tree is a domain object. * __extension__: An extension is a unit of functionality exposed to the platform in a declarative fashion by a bundle. The term 'extension category' is used to @@ -279,10 +279,10 @@ side-by-side without conflicting. # Framework -Open MCT Web is built on the [AngularJS framework]( http://www.angularjs.org ). A +Open MCT is built on the [AngularJS framework]( http://www.angularjs.org ). A good understanding of that framework is recommended. -Open MCT Web adds an extra layer on top of AngularJS to (a) generalize its +Open MCT adds an extra layer on top of AngularJS to (a) generalize its dependency injection mechanism slightly, particularly to handle many-to-one relationships; and (b) handle script loading. Combined, these features become a plugin mechanism. @@ -301,7 +301,7 @@ MCT Web.) are collected together in bundles, and may interact with other extensions. The framework layer, loaded and initiated from `index.html`, is the main point -of entry for an application built on Open MCT Web. It is responsible for wiring +of entry for an application built on Open MCT. It is responsible for wiring together the application at run time (much of this responsibility is actually delegated to Angular); at a high-level, the framework does this by proceeding through four stages: @@ -321,7 +321,7 @@ have been registered. ## Bundles -The basic configurable unit of Open MCT Web is the _bundle_. This term has been +The basic configurable unit of Open MCT is the _bundle_. This term has been used a bit already; now we'll get to a more formal definition. A bundle is a directory which contains: @@ -329,13 +329,13 @@ A bundle is a directory which contains: * A bundle definition; a file named `bundle.json`. * Subdirectories for sources, resources, and tests. * Optionally, a `README.md` Markdown file describing its contents (this is not -used by Open MCT Web in any way, but it's a helpful convention to follow.) +used by Open MCT in any way, but it's a helpful convention to follow.) The bundle definition is the main point of entry for the bundle. The framework looks at this to determine which components need to be loaded and how they interact. -A plugin in Open MCT Web is a bundle. The platform itself is also decomposed +A plugin in Open MCT is a bundle. The platform itself is also decomposed into bundles, each of which provides some category of functionality. The difference between a _bundle_ and a _plugin_ is purely a matter of the intended use; a plugin is just a bundle that is meant to be easily added or removed. When @@ -356,7 +356,7 @@ For instance, if `bundles.json` contained: "example/extensions" ] -...then the Open MCT Web framework would look for bundle definitions at +...then the Open MCT framework would look for bundle definitions at `example/builtins/bundle.json` and `example/extensions/bundle.json`, relative to the path of `index.html`. No other bundles would be loaded. @@ -457,7 +457,7 @@ arrays of extension definitions. ### General Extensions Extensions are intended as a general-purpose mechanism for adding new types of -functionality to Open MCT Web. +functionality to Open MCT. An extension category is registered with Angular under the name of the extension, plus a suffix of two square brackets; so, an Angular service (or, @@ -466,7 +466,7 @@ extensions, from all bundles, by including this string (e.g. `types[]` to get all type definitions) in a dependency declaration. As a convention, extension categories are given single-word, plural nouns for -names within Open MCT Web (e.g. `types`.) This convention is not enforced by the +names within Open MCT (e.g. `types`.) This convention is not enforced by the platform in any way. For extension categories introduced by external plugins, it is recommended to prefix the extension category with a vendor identifier (or similar) followed by a dot, to avoid collisions. @@ -505,7 +505,7 @@ the Angular-supported method for dependency injection is (effectively) constructor-style injection; so, both declared dependencies and run-time arguments are competing for space in a constructor's arguments. -To resolve this, the Open MCT Web framework registers extension instances in a +To resolve this, the Open MCT framework registers extension instances in a partially constructed form. That is, the constructor exposed by the extension's implementation is effectively decomposed into two calls; the first takes the dependencies, and returns the constructor in its second form, which takes the @@ -549,7 +549,7 @@ sorted according to these conventions when using them. ### Angular Built-ins Several entities supported Angular are expressed and managed as extensions in -Open MCT Web. Specifically, these extension categories are _directives_, +Open MCT. Specifically, these extension categories are _directives_, _controllers_, _services_, _constants_, _runs_, and _routes_. #### Angular Directives @@ -592,7 +592,7 @@ property value , which is the constant value that will be registered. In some cases, you want to register code to run as soon as the application starts; these can be registered as extensions of the [ runs category](https://docs.angularjs.org/api/ng/type/angular.Module#run ). Implementations registered in this category will be invoked (with their declared -dependencies) when the Open MCT Web application first starts. (Note that, in +dependencies) when the Open MCT application first starts. (Note that, in this case, the implementation is better thought of as just a function, as opposed to a constructor function.) @@ -627,13 +627,13 @@ providers of the same service (that is, with matching `provides` properties); for a decorator, this will be whichever provider, decorator, or aggregator is next in the sequence of decorators. -Services exposed by the Open MCT Web platform are often declared as composite +Services exposed by the Open MCT platform are often declared as composite services, as this form is open for a variety of common modifications. # Core API -Most of Open MCT Web's relevant API is provided and/or mediated by the -framework; that is, much of developing for Open MCT Web is a matter of adding +Most of Open MCT's relevant API is provided and/or mediated by the +framework; that is, much of developing for Open MCT is a matter of adding extensions which access other parts of the platform by means of dependency injection. @@ -642,9 +642,9 @@ to be passed along by other services. ## Domain Objects -Domain objects are the most fundamental component of Open MCT Web's information +Domain objects are the most fundamental component of Open MCT's information model. A domain object is some distinct thing relevant to a user's work flow, -such as a telemetry channel, display, or similar. Open MCT Web is a tool for +such as a telemetry channel, display, or similar. Open MCT is a tool for viewing, browsing, manipulating, and otherwise interacting with a graph of domain objects. @@ -681,7 +681,7 @@ exposed. ### Identifier Syntax For most purposes, a domain object identifier can be treated as a purely -symbolic string; these are typically generated by Open MCT Web and plug-ins +symbolic string; these are typically generated by Open MCT and plug-ins should rarely be concerned with its internal structure. A domain object identifier has one or two parts, separated by a colon. @@ -724,7 +724,7 @@ exposed it to be removed from its container. containing: * `name`: Human-readable name. * `description`: Human-readable summary of this action. - * `glyph`: Single character to be displayed in Open MCT Web's icon font set. + * `glyph`: Single character to be displayed in Open MCT's icon font set. * `context`: The context in which this action is being performed (see below) Action instances are typically obtained via a domain object's `action` @@ -740,7 +740,7 @@ dragged object in a drag-and-drop operation.) ## Telemetry -Telemetry series data in Open MCT Web is represented by a common interface, and +Telemetry series data in Open MCT is represented by a common interface, and packaged in a consistent manner to facilitate passing telemetry updates around multiple visualizations. @@ -753,7 +753,7 @@ is useful when multiple distinct data sources are in use side-by-side. * `key`: A machine-readable identifier for a unique series of telemetry within that source. * _Note: This API is still under development; additional properties, such as -start and end time, should be present in future versions of Open MCT Web._ +start and end time, should be present in future versions of Open MCT._ Additional properties may be included in telemetry requests which have specific interpretations for specific sources. @@ -777,7 +777,7 @@ not. (Typically, domain values are interpreted as UTC timestamps in milliseconds relative to the UNIX epoch.) A series must have at least one domain and one range, and may have more than one. -Telemetry series data in Open MCT Web is expressed via the following +Telemetry series data in Open MCT is expressed via the following `TelemetrySeries` interface: * `getPointCount()`: Returns the number of unique points/samples in this series. @@ -816,7 +816,7 @@ interface: * `getName()`: Get the human-readable name for this type. * `getDescription()`: Get a human-readable summary of this type. * `getGlyph()`: Get the single character to be rendered as an icon for this type -in Open MCT Web's custom font set. +in Open MCT's custom font set. * `getInitialModel()`: Get a domain object model that represents the initial state (before user specification of properties) for domain objects of this type. * `getDefinition()`: Get the extension definition for this type, as a JavaScript @@ -832,7 +832,7 @@ an array of `TypeProperty` instances. ### Type Features Features of a domain object type are expressed as symbolic string identifiers. -They are defined in practice by usage; currently, the Open MCT Web platform only +They are defined in practice by usage; currently, the Open MCT platform only uses the creation feature to determine which domain object types should appear in the Create menu. @@ -886,7 +886,7 @@ Categories supported by the platform include: * `key`: A machine-readable identifier for this action. * `name`: A human-readable name for this action (e.g. to show in a menu) * `description`: A human-readable summary of the behavior of this action. -* `glyph`: A single character which will be rendered in Open MCT Web's custom +* `glyph`: A single character which will be rendered in Open MCT's custom font set as an icon for this action. ## Capabilities Category @@ -997,7 +997,7 @@ of unremoved listeners. ## Indicators Category An indicator is an element that should appear in the status area at the bottom -of a running Open MCT Web client instance. +of a running Open MCT client instance. ### Standard Indicators @@ -1007,7 +1007,7 @@ provide implementations with the following methods: * `getText()`: Provides the human-readable text that will be displayed for this indicator. * `getGlyph()`: Provides a single-character string that will be displayed as an -icon in Open MCT Web's custom font set. +icon in Open MCT's custom font set. * `getDescription()`: Provides a human-readable summary of the current state of this indicator; will be displayed in a tooltip on hover. * `getClass()`: Get a CSS class that will be applied to this indicator. @@ -1033,7 +1033,7 @@ this variety do not need to provide an implementation. ## Licenses Category The extension category `licenses` can be used to add entries into the 'Licensing -information' page, reachable from Open MCT Web's About dialog. +information' page, reachable from Open MCT's About dialog. Licenses may have the following properties, all of which are strings: @@ -1046,11 +1046,11 @@ Licenses may have the following properties, all of which are strings: ## Policies Category -Policies are used to handle decisions made using Open MCT Web's `policyService`; +Policies are used to handle decisions made using Open MCT's `policyService`; examples of these decisions are determining the applicability of certain actions, or checking whether or not a domain object of one type can contain a domain object of a different type. See the section on the Policies for an -overview of Open MCT Web's policy model. +overview of Open MCT's policy model. A policy's extension definition should include: @@ -1066,7 +1066,7 @@ context)`. The specific types used for `candidate` and `context` vary by policy category; in general, what is being asked is 'is this candidate allowed in this context?' This method should return a boolean value. -Open MCT Web's policy model requires consensus; a policy decision is allowed +Open MCT's policy model requires consensus; a policy decision is allowed when and only when all policies choose to allow it. As such, policies should generally be written to reject a certain case, and allow (by returning `true`) anything else. @@ -1195,7 +1195,7 @@ Templates do not have implementations. ## Types Category The types extension category describes types of domain objects which may -appear within Open MCT Web. +appear within Open MCT. A type's extension definition should have the following properties: @@ -1203,7 +1203,7 @@ A type's extension definition should have the following properties: stored to and matched against the type property of domain object models. * `name`: The human-readable name for this domain object type. * `description`: A human-readable summary of this domain object type. -* `glyph`: A single character to be rendered as an icon in Open MCT Web's custom +* `glyph`: A single character to be rendered as an icon in Open MCT's custom font set. * `model`: A domain object model, used as the initial state for created domain objects of this type (before any properties are specified.) @@ -1252,7 +1252,7 @@ utilized via `mct-representation`); additionally: * `name`: The human-readable name for this view type. * description : A human-readable summary of this view type. -* `glyph`: A single character to be rendered as an icon in Open MCT Web's custom +* `glyph`: A single character to be rendered as an icon in Open MCT's custom font set. * `type`: Optional; if present, this representation is only applicable for domain object's of this type. @@ -1294,7 +1294,7 @@ are visible, and what state they manage and/or behavior they invoke. This set may contain up to two different objects: The _view proxy_, which is used to make changes to the view as a whole, and the _selected object_, which is -used to represent some state within the view. (Future versions of Open MCT Web +used to represent some state within the view. (Future versions of Open MCT may support multiple selected objects.) The `selection` object made available during Edit mode has the following @@ -1330,14 +1330,14 @@ are supported: # Directives -Open MCT Web defines several Angular directives that are intended for use both +Open MCT defines several Angular directives that are intended for use both internally within the platform, and by plugins. ## Before Unload The `mct-before-unload` directive is used to listen for (and prompt for user confirmation) of navigation changes in the browser. This includes reloading, -following links out of Open MCT Web, or changing routes. It is used to hook into +following links out of Open MCT, or changing routes. It is used to hook into both `onbeforeunload` event handling as well as route changes from within Angular. @@ -1449,7 +1449,7 @@ Passed as plain text in the attribute. ### Form Structure -Forms in Open MCT Web have a common structure to permit consistent display. A +Forms in Open MCT have a common structure to permit consistent display. A form is broken down into sections, which will be displayed in groups; each section is broken down into rows, each of which provides a control for a single property. Input from this form is two-way bound to the object passed via @@ -1658,7 +1658,7 @@ by scrolling to the bottom of the table rows. # Services -The Open MCT Web platform provides a variety of services which can be retrieved +The Open MCT platform provides a variety of services which can be retrieved and utilized via dependency injection. These services fall into two categories: * _Composite Services_ are defined by a set of components extensions; plugins may @@ -1670,7 +1670,7 @@ utilized by plugins but are not intended to be modified or augmented. ## Composite Type Services -This section describes the composite services exposed by Open MCT Web, +This section describes the composite services exposed by Open MCT, specifically focusing on their interface and contract. In many cases, the platform will include a provider for a service which consumes @@ -1988,7 +1988,7 @@ The `workerService` may be used to run web workers defined via the as a shared worker); if the `key` is unknown, returns `undefined`. # Models -Domain object models in Open MCT Web are JavaScript objects describing the +Domain object models in Open MCT are JavaScript objects describing the persistent state of the domain objects they describe. Their contents include a mix of commonly understood metadata attributes; attributes which are recognized by and/or determine the applicability of specific extensions; and properties @@ -2004,7 +2004,7 @@ MCT Web and can be utilized directly: ## Extension-specific Properties Other properties of domain object models have specific meaning imposed by other -extensions within the Open MCT Web platform. +extensions within the Open MCT platform. ### Capability-specific Properties @@ -2288,7 +2288,7 @@ way of its `composition` capability.) # Policies -Policies are consulted to determine when certain behavior in Open MCT Web is +Policies are consulted to determine when certain behavior in Open MCT is allowed. Policy questions are assigned to certain categories, which broadly describe the type of decision being made; within each category, policies have a candidate (the thing which may or may not be allowed) and, optionally, a context @@ -2313,13 +2313,13 @@ The candidate argument is the view's extension definition; the context argument is the `DomainObject` to be viewed. # Build-Test-Deploy -Open MCT Web is designed to support a broad variety of build and deployment +Open MCT is designed to support a broad variety of build and deployment options. The sources can be deployed in the same directory structure used during development. A few utilities are included to support development processes. ## Command-line Build -Open MCT Web is built using [`npm`](http://npmjs.com/) +Open MCT is built using [`npm`](http://npmjs.com/) and [`gulp`](http://gulpjs.com/). To install build dependencies (only needs to be run once): @@ -2331,12 +2331,12 @@ To build: `npm run prepublish` This will compile and minify JavaScript sources, as well as copy over assets. -The contents of the `dist` folder will contain a runnable Open MCT Web +The contents of the `dist` folder will contain a runnable Open MCT instance (e.g. by starting an HTTP server in that directory), including: -* A `main.js` file containing Open MCT Web source code. +* A `main.js` file containing Open MCT source code. * Various assets in the `example` and `platform` directories. -* An `index.html` that runs Open MCT Web in its default configuration. +* An `index.html` that runs Open MCT in its default configuration. Additional `gulp` tasks are defined in [the gulpfile](gulpfile.js). @@ -2345,7 +2345,7 @@ download build dependencies. ## Test Suite -Open MCT Web uses [Jasmine 1.3](http://jasmine.github.io/) and +Open MCT uses [Jasmine 1.3](http://jasmine.github.io/) and [Karma](http://karma-runner.github.io) for automated testing. The test suite is configured to load any scripts ending with `Spec.js` found @@ -2383,8 +2383,8 @@ information using [Blanket.JS](http://blanketjs.org/) and display this at the bottom of the screen. Currently, only statement coverage is displayed. ## Deployment -Open MCT Web is built to be flexible in terms of the deployment strategies it -supports. In order to run in the browser, Open MCT Web needs: +Open MCT is built to be flexible in terms of the deployment strategies it +supports. In order to run in the browser, Open MCT needs: 1. HTTP access to sources/resources for the framework, platform, and all active bundles. @@ -2393,13 +2393,13 @@ external services need to support HTTP or some other web-accessible interface, like WebSockets.) Any HTTP server capable of serving flat files is sufficient for the first point. -The command-line build also packages Open MCT Web into a `.war` file for easier +The command-line build also packages Open MCT into a `.war` file for easier deployment on containers such as Apache Tomcat. The second point may be less flexible, as it depends upon the specific services -to be utilized by Open MCT Web. Because of this, it is often the set of external +to be utilized by Open MCT. Because of this, it is often the set of external services (and the manner in which they are exposed) that determine how to deploy -Open MCT Web. +Open MCT. One important constraint to consider in this context is the browser's same origin policy. If external services are not on the same apparent host and port @@ -2416,7 +2416,7 @@ configuration does not create a security vulnerability. Examples of deployment strategies (and the conditions under which they make the most sense) include: -* If the external services that Open MCT Web will utilize are all running on +* If the external services that Open MCT will utilize are all running on [Apache Tomcat](https://tomcat.apache.org/), then it makes sense to run Open MCT Web from the same Tomcat instance as a separate web application. The `.war` artifact produced by the command line build facilitates this deployment @@ -2427,28 +2427,28 @@ hosts/ports, then it may make sense to use a web server that supports proxying, such as the [Apache HTTP Server](http://httpd.apache.org/). In this configuration, the HTTP server would be configured to proxy (or reverse proxy) requests at specific paths to the various external services, while providing -Open MCT Web as flat files from a different path. +Open MCT as flat files from a different path. * If a single server component is being developed to handle all server-side -needs of an Open MCT Web instance, it can make sense to serve Open MCT Web (as +needs of an Open MCT instance, it can make sense to serve Open MCT (as flat files) from the same component using an embedded HTTP server such as [Nancy](http://nancyfx.org/). * If no external services are needed (or if the 'external services' will just be generating flat files to read) it makes sense to utilize a lightweight flat file HTTP server such as [Lighttpd](http://www.lighttpd.net/). In this -configuration, Open MCT Web sources/resources would be placed at one path, while +configuration, Open MCT sources/resources would be placed at one path, while the files generated by the external service are placed at another path. * If all external services support CORS, it may make sense to have an HTTP -server that is solely responsible for making Open MCT Web sources/resources -available, and to have Open MCT Web contact these external services directly. +server that is solely responsible for making Open MCT sources/resources +available, and to have Open MCT contact these external services directly. Again, lightweight HTTP servers such as [Lighttpd](http://www.lighttpd.net/) are useful in this circumstance. The downside of this option is that additional configuration effort is required, both to enable CORS on the external services, -and to ensure that Open MCT Web can correctly locate these services. +and to ensure that Open MCT can correctly locate these services. -Another important consideration is authentication. By design, Open MCT Web does +Another important consideration is authentication. By design, Open MCT does not handle user authentication. Instead, this should typically be treated as a deployment-time concern, where authentication is handled by the HTTP server -which provides Open MCT Web, or an external access management system. +which provides Open MCT, or an external access management system. ### Configuration In most of the deployment options above, some level of configuration is likely @@ -2456,7 +2456,7 @@ to be needed or desirable to make sure that bundles can reach the external services they need to reach. Most commonly this means providing the path or URL to an external service. -Configurable parameters within Open MCT Web are specified via constants +Configurable parameters within Open MCT are specified via constants (literally, as extensions of the `constants` category) and accessed via dependency injection by the scripts which need them. Reasonable defaults for these constants are provided in the bundle where they are used. Plugins are @@ -2475,7 +2475,7 @@ for error, but is viable if there are a small number of constants to change. constants. This is particularly appropriate when multiple configurations (e.g. development, test, production) need to be managed easily; these can be swapped quickly by changing the set of active bundles in bundles.json. -* Deploy Open MCT Web and its external services in such a fashion that the +* Deploy Open MCT and its external services in such a fashion that the default paths to reach external services are all correct. ### Configuration Constants @@ -2486,7 +2486,7 @@ The following constants have global significance: to be overridden by other bundles, but persistence adapters may wish to consume this constant in order to provide persistence for that space. -The following configuration constants are recognized by Open MCT Web bundles: +The following configuration constants are recognized by Open MCT bundles: * Common UI elements - `platform/commonUI/general` * `THEME`: A string identifying the current theme symbolically. Individual stylesheets (the `stylesheets` extension category) may specify an optional diff --git a/docs/src/index.md b/docs/src/index.md index dbb1d36220..83860e0da6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,13 +1,13 @@ -# Open MCT Web Documentation +# Open MCTDocumentation ## Overview Documentation is provided to support the use and development of - Open MCT Web. It's recommended that before doing - any development with Open MCT Web you take some time to familiarize yourself + Open MCT. It's recommended that before doing + any development with Open MCT you take some time to familiarize yourself with the documentation below. - Open MCT Web provides functionality out of the box, but it's also a platform for + Open MCT provides functionality out of the box, but it's also a platform for building rich mission operations applications based on modern web technology. The platform is configured declaratively, and defines conventions for building on the provided capabilities by creating modular 'bundles' that @@ -17,7 +17,7 @@ ## Sections * The [Architecture Overview](architecture/) describes the concepts used - throughout Open MCT Web, and gives a high level overview of the platform's design. + throughout Open MCT, and gives a high level overview of the platform's design. * The [Developer's Guide](guide/) goes into more detail about how to use the platform and the functionality that it provides. @@ -31,5 +31,4 @@ functions that make up the software platform. * Finally, the [Development Process](process/) document describes the - Open MCT Web software development cycle. - \ No newline at end of file + Open MCT software development cycle. diff --git a/docs/src/process/cycle.md b/docs/src/process/cycle.md index 4618fe0433..b07a76e8eb 100644 --- a/docs/src/process/cycle.md +++ b/docs/src/process/cycle.md @@ -1,6 +1,6 @@ # Development Cycle -Development of Open MCT Web occurs on an iterative cycle of +Development of Open MCT occurs on an iterative cycle of sprints and releases. * A _sprint_ is three weeks in duration, and represents a diff --git a/docs/src/process/index.md b/docs/src/process/index.md index 78f919e4c2..fc8f7e6e91 100644 --- a/docs/src/process/index.md +++ b/docs/src/process/index.md @@ -1,6 +1,6 @@ # Development Process -The process used to develop Open MCT Web is described in the following +The process used to develop Open MCT is described in the following documents: * The [Development Cycle](cycle.md) describes how and when specific @@ -9,7 +9,7 @@ documents: Open MCT (both semantics and process.) * Testing is described in two documents: * The [Test Plan](testing/plan.md) summarizes the approaches used - to test Open MCT Web. + to test Open MCT. * The [Test Procedures](testing/procedures.md) document what specific tests are performed to verify correctness, and how they should be carried out. diff --git a/docs/src/process/testing/plan.md b/docs/src/process/testing/plan.md index fead5f5a50..47ab60ee34 100644 --- a/docs/src/process/testing/plan.md +++ b/docs/src/process/testing/plan.md @@ -2,7 +2,7 @@ ## Test Levels -Testing for Open MCT Web includes: +Testing for Open MCT includes: * _Smoke testing_: Brief, informal testing to verify that no major issues or regressions are present in the software, or in specific features of diff --git a/docs/src/process/testing/procedures.md b/docs/src/process/testing/procedures.md index b33f88c6d1..5ccf0def5b 100644 --- a/docs/src/process/testing/procedures.md +++ b/docs/src/process/testing/procedures.md @@ -4,7 +4,7 @@ This document is intended to be used: -* By testers, to verify that Open MCT Web behaves as specified. +* By testers, to verify that Open MCT behaves as specified. * By the development team, to document new test cases and to provide guidance on how to author these. @@ -62,7 +62,7 @@ Test cases should be narrow in scope; if a list of steps is excessively long (or must be written vaguely to be kept short) it should be broken down into multiple tests which reference one another. -All requirements satisfied by Open MCT Web should be verifiable using +All requirements satisfied by Open MCT should be verifiable using one or more test procedures. ## Glossary @@ -166,4 +166,4 @@ Eval. criteria | Visual inspection * Logs should not contain any unexpected warnings or errors ("expected" warnings or errors are those that have been documented and prioritized as known issues, or those that are explained by transient conditions - external to the software, such as network outages.) \ No newline at end of file + external to the software, such as network outages.) diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md index 1bda1d8848..2ebc32b957 100644 --- a/docs/src/tutorials/index.md +++ b/docs/src/tutorials/index.md @@ -1,4 +1,4 @@ -# Open MCT Web Tutorials +# Open MCT Tutorials Victor Woeltjen victor.woeltjen@nasa.gov @@ -23,9 +23,9 @@ been added or removed as part of the tutorial. In these cases, any lines added will be indicated with a '+' at the start of the line. Any lines removed will be indicated with a '-'. -## Setting Up Open MCT Web +## Setting Up Open MCT -In this section, we will cover the steps necessary to get a minimal Open MCT Web +In this section, we will cover the steps necessary to get a minimal Open MCT developer environment up and running. Once we have this, we will be able to proceed with writing plugins as described in this tutorial. @@ -40,22 +40,22 @@ more recent versions, but this cannot be guaranteed. * Google Chrome v42: https://www.google.com/chrome/ * A text editor. -Open MCT Web can be run without any of these tools, provided suitable -alternatives are taken; see the [Open MCT Web Developer Guide](../guide/index.md) -for a more general overview of how to run and deploy a Open MCT Web application. +Open MCT can be run without any of these tools, provided suitable +alternatives are taken; see the [Open MCT Developer Guide](../guide/index.md) +for a more general overview of how to run and deploy a Open MCT application. -### Check out Open MCT Web Sources +### Check out Open MCT Sources -First step is to check out Open MCT Web from the source repository. +First step is to check out Open MCT from the source repository. `git clone https://github.com/nasa/openmctweb.git openmctweb` -This will create a copy of the Open MCT Web source code repository in the folder +This will create a copy of the Open MCT source code repository in the folder `openmctweb` (relative to the path from which you ran the command.) If you have a repository URL, use that as the "path to repo" above. Alternately, -if you received Open MCT Web as a git bundle, the path to that bundle on the +if you received Open MCT as a git bundle, the path to that bundle on the local filesystem can be used instead. -At this point, it will also be useful to branch off of Open MCT Web v0.6.2 +At this point, it will also be useful to branch off of Open MCT v0.6.2 (which was used when writing these tutorials) to begin adding plugins. cd openmctweb @@ -64,12 +64,12 @@ At this point, it will also be useful to branch off of Open MCT Web v0.6.2 ### Configuring Persistence -In its default configuration, Open MCT Web will try to use ElasticSearch +In its default configuration, Open MCT will try to use ElasticSearch (expected to be deployed at /elastic on the same HTTP server running Open MCT Web) to persist user-created domain objects. We don't need that for these tutorials, so we will replace the ElasticSearch plugin with the example persistence plugin. This doesn't actually persist, so anything we create within -Open MCT Web will be lost on reload, but that's fine for purposes of these +Open MCT will be lost on reload, but that's fine for purposes of these tutorials. To change this configuration, edit bundles.json (at the top level of the Open @@ -132,7 +132,7 @@ __bundles.json__ ### Run a Web Server -The next step is to run a web server so that you can view the Open MCT Web +The next step is to run a web server so that you can view the Open MCT client (including the plugins you add to it) in browser. Any web server can be used for hosting OpenMCTWeb, and a trivial web server is provided in this package for the purposes of running the tutorials. The provided web server @@ -144,11 +144,11 @@ To run the tutorial web server ### Viewing in Browser -Once running, you should be able to view Open MCT Web from your browser at +Once running, you should be able to view Open MCT from your browser at http://localhost:8080/ (assuming the web server is running on port 8080, and OpenMCTWeb is installed at the server's root path). [Google Chrome](https://www.google.com/chrome/) is recommended for these -tutorials, as Chrome is Open MCT Web's "test-to" browser. The browser cache +tutorials, as Chrome is Open MCT's "test-to" browser. The browser cache can sometimes interfere with development (masking changes by using older versions of sources); to avoid this, it is easiest to run Chrome with Developer Tools expanded, and "Disable cache" selected from the Network @@ -158,7 +158,7 @@ tab, as shown below. # Tutorials -These tutorials cover three of the common tasks in Open MCT Web: +These tutorials cover three of the common tasks in Open MCT: * The "to-do list" tutorial illustrates how to add a new application feature. * The "bar graph" tutorial illustrates how to add a new telemetry visualization. @@ -167,17 +167,17 @@ backend. ## To-do List -The goal of this tutorial is to add a new application feature to Open MCT Web: +The goal of this tutorial is to add a new application feature to Open MCT: To-do lists. Users should be able to create and manage these to track items that they need to do. This is modelled after the to-do lists at http://todomvc.com/. ### Step 1-Create the Plugin -The first step to adding a new feature to Open MCT Web is to create the plugin -which will expose that feature. A plugin in Open MCT Web is represented by what +The first step to adding a new feature to Open MCT is to create the plugin +which will expose that feature. A plugin in Open MCT is represented by what is called a bundle; a bundle, in turn, is a directory which contains a file bundle.json, which in turn describes where other relevant sources & resources -will be. The syntax of this file is described in more detail in the Open MCT Web +will be. The syntax of this file is described in more detail in the Open MCT Developer Guide. We will create this file in the directory tutorials/todo (we can hereafter refer @@ -254,7 +254,7 @@ __bundles.json__ ``` __bundles.json__ -At this point, we can reload Open MCT Web. We haven't introduced any new +At this point, we can reload Open MCT. We haven't introduced any new functionality, so we don't see anything different, but if we run with logging enabled ( http://localhost:8080/?log=info ) and check the browser console, we should see: @@ -265,11 +265,11 @@ should see: ### Step 2-Add a Domain Object Type -Features in a Open MCT Web application are most commonly expressed as domain +Features in a Open MCT application are most commonly expressed as domain objects and/or views thereof. A domain object is some thing that is relevant to -the work that the Open MCT Web application is meant to support. Domain objects +the work that the Open MCT application is meant to support. Domain objects can be created, organized, edited, placed in layouts, and so forth. (For a -deeper explanation of domain objects, see the Open MCT Web Developer Guide.) +deeper explanation of domain objects, see the Open MCT Developer Guide.) In the case of our to-do list feature, the to-do list itself is the thing we'll want users to be able to create and edit. So, we will add that as a new type in @@ -303,7 +303,7 @@ Going through the properties we've defined: domain objects of this type. * The `name` of "To-Do List" is the human-readable name for this type, and will be shown to users. -* The `glyph` refers to a special character in Open MCT Web's custom font set; +* The `glyph` refers to a special character in Open MCT's custom font set; this will be used as an icon. * The `description` is also human-readable, and will be used whenever a longer explanation of what this type is should be shown. @@ -312,7 +312,7 @@ this type. Including `creation` here means that we want users to be able to create this (in other cases, we may wish to expose things as domain objects which aren't user-created, in which case we would omit this.) -If we reload Open MCT Web, we see that our new domain object type appears in the +If we reload Open MCT, we see that our new domain object type appears in the Create menu: ![To-Do List](images/todo.png) @@ -324,10 +324,10 @@ because we haven't defined any yet. ### Step 3-Add a View In order to allow a to-do list to be used, we need to define and display its -contents. In Open MCT Web, the pattern that the user expects is that they'll +contents. In Open MCT, the pattern that the user expects is that they'll click on an object in the left-hand tree, and see a visualization of it to the -right; in Open MCT Web, these visualizations are called views. -A view in Open MCT Web is defined by an Angular template. We'll add that in the +right; in Open MCT, these visualizations are called views. +A view in Open MCT is defined by an Angular template. We'll add that in the directory `tutorials/todo/res/templates` (`res` is, by default, the directory where bundle-related resources are kept, and `templates` is where HTML templates are stored by convention.) @@ -357,12 +357,12 @@ to filter down to either complete or incomplete tasks. of the domain object being viewed; this contains all of the persistent state associated with that object. This model is effectively just a JSON document, so we can choose what goes into it (so long as we take care not to collide with -platform-defined properties; see the Open MCT Web Developer Guide.) Here, we +platform-defined properties; see the Open MCT Developer Guide.) Here, we assume that all tasks will be stored in a property `tasks`, and that each will be an object containing a `description` (the readable summary of the task) and a boolean `completed` flag. -To expose this view in Open MCT Web, we need to declare it in our bundle +To expose this view in Open MCT, we need to declare it in our bundle definition: ```diff @@ -399,7 +399,7 @@ contains the following properties: * Its `key` is its machine-readable name; we've given it the same name here as the domain object type, but could have chosen any unique name. -* The `type` property tells Open MCT Web that this view is only applicable to +* The `type` property tells Open MCT that this view is only applicable to domain objects of that type. This means that we'll see this view for To-do Lists that we create, but not for other domain objects (such as Folders.) @@ -449,10 +449,10 @@ definition of that type. ``` __tutorials/todo/bundle.json__ -Now, when To-do List objects are created in Open MCT Web, they will initially +Now, when To-do List objects are created in Open MCT, they will initially have the state described by that model property. -If we reload Open MCT Web, create a To-do List, and navigate to it in the tree, +If we reload Open MCT, create a To-do List, and navigate to it in the tree, we should now see: ![To-Do List](images/todo-list.png) @@ -527,7 +527,7 @@ first argument is falsy.) * `toggleCompletion` changes whether or not a task is complete. We make the change via the domain object's `mutation` capability, and then persist the -change via its `persistence` capability. See the Open MCT Web Developer Guide +change via its `persistence` capability. See the Open MCT Developer Guide for more information on these capabilities. * `showTask` is meant to be used to help decide if a task should be shown, based @@ -537,7 +537,7 @@ the use of the double-not !! to coerce the completed flag to a boolean, for equality testing.) Note that these functions make reference to `$scope.domainObject;` this is the -domain object being viewed, which is passed into the scope by Open MCT Web +domain object being viewed, which is passed into the scope by Open MCT prior to our template being utilized. On its own, this controller merely exposes these functions; the next step is to @@ -640,7 +640,7 @@ if we go to My Items and come back. We now have a somewhat-functional view of our To-Do List, but we're still missing some important functionality: Adding and removing tasks! -This is a good place to discuss the user interface style of Open MCT Web. Open +This is a good place to discuss the user interface style of Open MCT. Open MCT Web draws a distinction between "using" and "editing" a domain object; in general, you can only make changes to a domain object while in Edit mode, which is reachable from the button with a pencil icon. This distinction helps users @@ -732,14 +732,14 @@ What we've stated here is that the To-Do List's view will have a toolbar which contains two sections (which will be visually separated by a divider), each of which contains one button. The first is a button labelled "Add Task" that will invoke an `addTask` method; the second is a button with a glyph (which will appear -as a trash can in Open MCT Web's custom font set) which will invoke a `removeTask` -method. For more information on forms and tool bars in Open MCT Web, see the -Open MCT Web Developer Guide. +as a trash can in Open MCT's custom font set) which will invoke a `removeTask` +method. For more information on forms and tool bars in Open MCT, see the +Open MCT Developer Guide. -If we reload and run Open MCT Web, we won't see any tool bar when we switch over +If we reload and run Open MCT, we won't see any tool bar when we switch over to Edit mode. This is because the aforementioned methods are expected to be found on currently-selected elements; we haven't done anything with selections -in our view yet, so the Open MCT Web platform will filter this tool bar down to +in our view yet, so the Open MCT platform will filter this tool bar down to all the applicable controls, which means no controls at all. To support selection, we will need to make some changes to our controller: @@ -842,7 +842,7 @@ click the _Add Task_ button. This form is described declaratively, and populates an object that has the same format as tasks in the `tasks` array of our To-Do List's model. * We've added an argument to the `TodoController`: The `dialogService`, which is -exposed by the Open MCT Web platform to handle showing dialogs. +exposed by the Open MCT platform to handle showing dialogs. * Some utility functions for handling the actual adding and removing of tasks. These use the `mutation` capability to modify the tasks in the To-Do List's model. @@ -947,7 +947,7 @@ declare that dependency in its extension definition: ``` __tutorials/todo/bundle.json__ -If we now reload Open MCT Web, we'll be able to see the new functionality we've +If we now reload Open MCT, we'll be able to see the new functionality we've added. If we Create a new To-Do List, navigate to it, and click the button with the Pencil icon in the top-right, we'll be in edit mode. We see, first, that our "Add Task" button appears in the tool bar: @@ -1136,7 +1136,7 @@ Here, we have defined classes and appearances for: * A message, which we will add next, to display when there are no tasks (`example-message`). -To include this CSS file in our running instance of Open MCT Web, we need to +To include this CSS file in our running instance of Open MCT, we need to declare it in our bundle definition, this time as an extension of category `stylesheets`: ```diff @@ -1436,7 +1436,7 @@ The corresponding CSS file which styles and positions these elements: __tutorials/bargraph/res/css/bargraph.css__ This is already enough that, if we add `"tutorials/bargraph"` to `bundles.json`, -we should be able to run Open MCT Web and see our Bar Graph as an available view +we should be able to run Open MCT and see our Bar Graph as an available view for domain objects which provide telemetry (such as the example _Sine Wave Generator_) as well as for _Telemetry Panel_ objects: @@ -1502,7 +1502,7 @@ will help support some positioning in the template. to real-time telemetry updates. This will deal with most of the complexity of dealing with telemetry (e.g. differentiating between individual telemetry points and telemetry panels, monitoring latest values) and provide us with a useful -interface for populating our view. The the Open MCT Web Developer Guide for more +interface for populating our view. The the Open MCT Developer Guide for more information on dealing with telemetry. Whenever the telemetry handler invokes its callbacks, we update the set of @@ -1594,7 +1594,7 @@ service we made use of. ``` __tutorials/bargraph/bundle.json__ -When we reload Open MCT Web, we are now able to see that our bar graph view +When we reload Open MCT, we are now able to see that our bar graph view correctly labels one bar per telemetry-providing domain object, as shown for this Telemetry Panel containing four Sine Wave Generators. @@ -1703,7 +1703,7 @@ __tutorials/bargraph/res/templates/bargraph.html__ Here, we utilize the functions we just provided from the controller to position the bar, using an ng-style attribute. -When we reload Open MCT Web, our bar graph view now looks like: +When we reload Open MCT, our bar graph view now looks like: ![Bar Plot](images/bar-plot-3.png) @@ -1714,7 +1714,7 @@ sine waves, but what about other values? We want to provide the user with a means of configuring these boundaries. This is normally done via Edit mode. Since view configuration is a common -problem, the Open MCT Web platform exposes a configuration object - called +problem, the Open MCT platform exposes a configuration object - called `configuration` - into our view's scope. We can populate it as we please, and when we return to our view later, those changes will be persisted. @@ -1884,14 +1884,14 @@ defaults (if needed), and expose its state into the scope. and `high` as entered by the user from the tool bar. This uses the getter-setters we defined previously. -If we reload Open MCT Web and go to a Bar Graph view in Edit mode, we now see +If we reload Open MCT and go to a Bar Graph view in Edit mode, we now see that we can change these bounds from the tool bar. ![Bar plot](images/bar-plot-4.png) ## Telemetry Adapter -The goal of this tutorial is to demonstrate how to integrate Open MCT Web +The goal of this tutorial is to demonstrate how to integrate Open MCT with an existing telemetry system. A summary of the steps we will take: @@ -1902,7 +1902,7 @@ A summary of the steps we will take: ### Step 0-Expose Your Telemetry -As a precondition to integrating telemetry data into Open MCT Web, this +As a precondition to integrating telemetry data into Open MCT, this information needs to be available over web-based interfaces. In practice, this will most likely mean exposing data over HTTP, or over WebSockets. For purposes of this tutorial, a simple node server is provided to stand @@ -2080,7 +2080,7 @@ measurement. (Note that the term "measurement" is used to describe a distinct data series within this system; in other systems, these have been called channels, mnemonics, telemetry points, or other names. No preference is made here; -Open MCT Web is easily adapted to use the terminology appropriate to your +Open MCT is easily adapted to use the terminology appropriate to your system.) Additionally, while running the server from the terminal we can toggle the state of the "spacecraft" by hitting enter; this will turn the "thrusters" @@ -2162,9 +2162,9 @@ telemetry. __tutorial-server/dictionary.json__ It should be noted that neither the interface for the example server nor the -dictionary format are expected by Open MCT Web; rather, these are intended to +dictionary format are expected by Open MCT; rather, these are intended to stand in for some existing source of telemetry data to which we wish to adapt -Open MCT Web. +Open MCT. We can run this example server by: @@ -2181,11 +2181,11 @@ like https://www.npmjs.com/package/wscat : < {"type":"dictionary","value":{"name":"Example Spacecraft","identifier":"sc","subsystems":[{"name":"Propulsion","identifier":"prop","measurements":[{"name":"Fuel","identifier":"prop.fuel","units":"kilograms","type":"float"},{"name":"Thrusters","identifier":"prop.thrusters","units":"None","type":"string"}]},{"name":"Communications","identifier":"comms","measurements":[{"name":"Received","identifier":"comms.recd","units":"bytes","type":"integer"},{"name":"Sent","identifier":"comms.sent","units":"bytes","type":"integer"}]},{"name":"Power","identifier":"pwr","measurements":[{"name":"Generator Temperature","identifier":"pwr.temp","units":"€C","type":"float"},{"name":"Generator Current","identifier":"pwr.c","units":"A","type":"float"},{"name":"Generator Voltage","identifier":"pwr.v","units":"V","type":"float"}]}]}} Now that the example server's interface is reasonably well-understood, a plugin -can be written to adapt Open MCT Web to utilize it. +can be written to adapt Open MCT to utilize it. ### Step 1-Add a Top-level Object -Since Open MCT Web uses an "object-first" approach to accessing data, before +Since Open MCT uses an "object-first" approach to accessing data, before we'll be able to do anything with this new data source, we'll need to have a way to explore the available measurements in the tree. In this step, we will add a top-level object which will serve as a container; in the next step, we @@ -2276,7 +2276,7 @@ If we include this in our set of active bundles: __bundles.json__ -...we will be able to reload Open MCT Web and see that it is present: +...we will be able to reload Open MCT and see that it is present: ![Telemetry](images/telemetry-1.png) @@ -2287,7 +2287,7 @@ dictionary. In order to expose the telemetry dictionary, we first need to read it from the server. Our first step will be to add a service that will handle interactions -with the server; this will not be used by Open MCT Web directly, but will be +with the server; this will not be used by Open MCT directly, but will be used by subsequent components we add. /*global define,WebSocket*/ @@ -2340,7 +2340,7 @@ Once the dictionary has been loaded, we will want to represent its contents as domain objects. Specifically, we want subsystems to appear as objects under My Spacecraft, and measurements to appear as objects within those subsystems. This means that we need to convert the data from the dictionary -into domain object models, and expose these to Open MCT Web via a +into domain object models, and expose these to Open MCT via a `modelService`. /*global define*/ @@ -2449,16 +2449,16 @@ also prefix it with `example_tlm`:. This accomplishes a few things: * We can easily tell whether an identifier is expected to be in the dictionary or not. * We avoid naming collisions with other model providers. - * Finally, Open MCT Web uses the colon prefix as a hint that this domain + * Finally, Open MCT uses the colon prefix as a hint that this domain object will not be in the persistence store. * A couple of new types are introduced here (in the `type` field of the domain object models we create); we will need to define these as extensions as well in order for them to display correctly. -* The `composition` field of each subsystem contained the Open MCT Web +* The `composition` field of each subsystem contained the Open MCT identifiers of all the measurements in that subsystem. This `composition` field -will be used by Open MCT Web to determine what domain objects contain other +will be used by Open MCT to determine what domain objects contain other domain objects (e.g. to populate the tree.) -* The `telemetry` field of each measurement will be used by Open MCT Web to +* The `telemetry` field of each measurement will be used by Open MCT to understand how to request and interpret telemetry data for this object. The `key` is the machine-readable identifier for this measurement within the telemetry system; the `ranges` provide metadata about the values for this data. @@ -2637,7 +2637,7 @@ overridden if defined anywhere else, allowing configuration bundles to specify different URLs for the WebSocket connection. * The initializer script is registered using the `runs` category of extension, to ensure that this executes (and populates the contents of the top-level My -Spacecraft object) once Open MCT Web is started. +Spacecraft object) once Open MCT is started. * This depends upon the `example.adapter` service we exposed above, as well as Angular's `$q`; these services will be made available in the constructor call. @@ -2648,7 +2648,7 @@ this is registered under the extension category `components`. we exposed above, as well as Angular's `$q`; these services will be made available in the constructor call. -Now if we run Open MCT Web (assuming our example telemetry server is also +Now if we run Open MCT (assuming our example telemetry server is also running) and expand our top-level node completely, we see the contents of our dictionary: @@ -2793,7 +2793,7 @@ that will resolve only when all histories have been packaged. Promise-chaining is used to ensure that the resolved value will be the fully-packaged data. It is worth mentioning here that the `requests` we receive should look a little -familiar. When Open MCT Web generates a `request` object associated with a +familiar. When Open MCT generates a `request` object associated with a domain object, it does so by merging together three JavaScript objects: * First, the `telemetry` property from that domain object's type definition. @@ -2936,7 +2936,7 @@ back to see it. We can fix this by adding support for telemetry subscriptions. ### Step 4-Real-time Telemetry Finally, we want to utilize the server's ability to subscribe to telemetry -from Open MCT Web. To do this, first we want to expose some new methods for +from Open MCT. To do this, first we want to expose some new methods for this from our server adapter: ```diff @@ -3116,6 +3116,6 @@ we issue an unsubscribe request. (We don't take any care to avoid issuing multiple subscribe requests to the server, because we happen to know that the server can handle this.) -Running Open MCT Web again, we can still plot our historical telemetry - but +Running Open MCT again, we can still plot our historical telemetry - but now we also see that it updates in real-time as more data comes in from the server. From 5279c842f55b1efeb33421dea1d98fc855b5983f Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 4 May 2016 10:17:23 -0700 Subject: [PATCH 33/38] [Branding] Change title of about dialog --- platform/commonUI/about/res/templates/about-dialog.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/commonUI/about/res/templates/about-dialog.html b/platform/commonUI/about/res/templates/about-dialog.html index 7b7dd436f5..bd87ce72c1 100644 --- a/platform/commonUI/about/res/templates/about-dialog.html +++ b/platform/commonUI/about/res/templates/about-dialog.html @@ -22,7 +22,7 @@
-

OpenMCT Web

+

Open MCT

Open MCT Web, Copyright © 2014-2015, United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.

Open MCT Web 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.

From 00706558f5abcde2dcfee825dcf737c929f75751 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 4 May 2016 10:42:46 -0700 Subject: [PATCH 34/38] [Branding] Restore missing space --- docs/src/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/index.md b/docs/src/index.md index 83860e0da6..3b4d767106 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,4 +1,4 @@ -# Open MCTDocumentation +# Open MCT Documentation ## Overview From 1c6ef28b80ed6972e5df6ad9103e1a0f7e13351c Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 4 May 2016 11:13:12 -0700 Subject: [PATCH 35/38] [Table] Fix headers in firefox Don't use table-cell displays for cells, resolves issues with zero-sized tbody causing thead to be 100% of table size. Fixes https://github.com/nasa/openmct/issues/809 --- platform/features/table/res/sass/table.scss | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/platform/features/table/res/sass/table.scss b/platform/features/table/res/sass/table.scss index a79cfac4c6..0e52f589e2 100644 --- a/platform/features/table/res/sass/table.scss +++ b/platform/features/table/res/sass/table.scss @@ -34,17 +34,28 @@ } .mct-table { table-layout: fixed; - th { - box-sizing: border-box; + thead { + display: block; + tr { + display: block; + white-space: nowrap; + th { + display: inline-block; + box-sizing: border-box; + } + } } tbody { tr { position: absolute; + white-space: nowrap; + display: block; } td { white-space: nowrap; overflow: hidden; box-sizing: border-box; + display: inline-block; } } -} \ No newline at end of file +} From 7fad4e9ea1fe085143eeb1b403e095399e554e2a Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Thu, 5 May 2016 18:25:37 -0700 Subject: [PATCH 36/38] [General] remove dupe, debounce inputs Remove duplicate ClickAwayController implementation that was obscuring actual implementation. Debounce clickaway action in case toggle is invoked in a click handler. Resolves https://github.com/nasa/openmct/issues/888 --- platform/commonUI/general/bundle.js | 4 +- .../src/controllers/ClickAwayController.js | 17 ++- .../controllers/ClickAwayControllerSpec.js | 22 ++-- platform/search/bundle.js | 10 -- .../src/controllers/ClickAwayController.js | 105 ------------------ .../controllers/ClickAwayControllerSpec.js | 94 ---------------- 6 files changed, 22 insertions(+), 230 deletions(-) delete mode 100644 platform/search/src/controllers/ClickAwayController.js delete mode 100644 platform/search/test/controllers/ClickAwayControllerSpec.js diff --git a/platform/commonUI/general/bundle.js b/platform/commonUI/general/bundle.js index 3e7f558e8b..8bbdbc77af 100644 --- a/platform/commonUI/general/bundle.js +++ b/platform/commonUI/general/bundle.js @@ -268,8 +268,8 @@ define([ "key": "ClickAwayController", "implementation": ClickAwayController, "depends": [ - "$scope", - "$document" + "$document", + "$timeout" ] }, { diff --git a/platform/commonUI/general/src/controllers/ClickAwayController.js b/platform/commonUI/general/src/controllers/ClickAwayController.js index 9c7c6f8091..a25ff51d49 100644 --- a/platform/commonUI/general/src/controllers/ClickAwayController.js +++ b/platform/commonUI/general/src/controllers/ClickAwayController.js @@ -19,7 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -/*global define,Promise*/ +/*global define*/ define( [], @@ -36,20 +36,19 @@ define( * @param $scope the scope in which this controller is active * @param $document the document element, injected by Angular */ - function ClickAwayController($scope, $document) { + function ClickAwayController($document, $timeout) { var self = this; this.state = false; - this.$scope = $scope; this.$document = $document; - // Callback used by the document listener. Deactivates; - // note also $scope.$apply is invoked to indicate that - // the state of this controller has changed. + // Callback used by the document listener. Timeout ensures that + // `clickaway` action occurs after `toggle` if `toggle` is + // triggered by a click/mouseup. this.clickaway = function () { - self.deactivate(); - $scope.$apply(); - return false; + $timeout(function () { + self.deactivate(); + }); }; } diff --git a/platform/commonUI/general/test/controllers/ClickAwayControllerSpec.js b/platform/commonUI/general/test/controllers/ClickAwayControllerSpec.js index 96e7b6c13f..9b73aefb51 100644 --- a/platform/commonUI/general/test/controllers/ClickAwayControllerSpec.js +++ b/platform/commonUI/general/test/controllers/ClickAwayControllerSpec.js @@ -19,7 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ +/*global define,describe,it,expect,beforeEach,jasmine*/ define( ["../../src/controllers/ClickAwayController"], @@ -27,20 +27,20 @@ define( "use strict"; describe("The click-away controller", function () { - var mockScope, - mockDocument, + var mockDocument, + mockTimeout, controller; beforeEach(function () { - mockScope = jasmine.createSpyObj( - "$scope", - [ "$apply" ] - ); mockDocument = jasmine.createSpyObj( "$document", [ "on", "off" ] ); - controller = new ClickAwayController(mockScope, mockDocument); + mockTimeout = jasmine.createSpy('timeout'); + controller = new ClickAwayController( + mockDocument, + mockTimeout + ); }); it("is initially inactive", function () { @@ -79,10 +79,12 @@ define( }); it("deactivates and detaches listener on document click", function () { - var callback; + var callback, timeout; controller.setState(true); callback = mockDocument.on.mostRecentCall.args[1]; callback(); + timeout = mockTimeout.mostRecentCall.args[0]; + timeout(); expect(controller.isActive()).toEqual(false); expect(mockDocument.off).toHaveBeenCalledWith("mouseup", callback); }); @@ -91,4 +93,4 @@ define( }); } -); \ No newline at end of file +); diff --git a/platform/search/bundle.js b/platform/search/bundle.js index 7a0a9f4b9e..9a0451aa9b 100644 --- a/platform/search/bundle.js +++ b/platform/search/bundle.js @@ -24,7 +24,6 @@ define([ "./src/controllers/SearchController", "./src/controllers/SearchMenuController", - "./src/controllers/ClickAwayController", "./src/services/GenericSearchProvider", "./src/services/SearchAggregator", "text!./res/templates/search-item.html", @@ -34,7 +33,6 @@ define([ ], function ( SearchController, SearchMenuController, - ClickAwayController, GenericSearchProvider, SearchAggregator, searchItemTemplate, @@ -73,14 +71,6 @@ define([ "$scope", "types[]" ] - }, - { - "key": "ClickAwayController", - "implementation": ClickAwayController, - "depends": [ - "$scope", - "$document" - ] } ], "representations": [ diff --git a/platform/search/src/controllers/ClickAwayController.js b/platform/search/src/controllers/ClickAwayController.js deleted file mode 100644 index 9b92e89cc0..0000000000 --- a/platform/search/src/controllers/ClickAwayController.js +++ /dev/null @@ -1,105 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web 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 Web 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. - *****************************************************************************/ -/*global define,Promise*/ - -/* - * Copied from the ClickAwayController in platform/commonUI/general - */ - -define( - [], - function () { - "use strict"; - - /** - * A ClickAwayController is used to toggle things (such as context - * menus) where clicking elsewhere in the document while the toggle - * is in an active state is intended to dismiss the toggle. - * - * @constructor - * @param $scope the scope in which this controller is active - * @param $document the document element, injected by Angular - */ - function ClickAwayController($scope, $document) { - var state = false, - clickaway; - - // Track state, but also attach and detach a listener for - // mouseup events on the document. - function deactivate() { - state = false; - $document.off("mouseup", clickaway); - } - - function activate() { - state = true; - $document.on("mouseup", clickaway); - } - - function changeState() { - if (state) { - deactivate(); - } else { - activate(); - } - } - - // Callback used by the document listener. Deactivates; - // note also $scope.$apply is invoked to indicate that - // the state of this controller has changed. - clickaway = function () { - deactivate(); - $scope.$apply(); - return false; - }; - - return { - /** - * Get the current state of the toggle. - * @return {boolean} true if active - */ - isActive: function () { - return state; - }, - /** - * Set a new state for the toggle. - * @return {boolean} true to activate - */ - setState: function (newState) { - if (state !== newState) { - changeState(); - } - }, - /** - * Toggle the current state; activate if it is inactive, - * deactivate if it is active. - */ - toggle: function () { - changeState(); - } - }; - - } - - return ClickAwayController; - } -); \ No newline at end of file diff --git a/platform/search/test/controllers/ClickAwayControllerSpec.js b/platform/search/test/controllers/ClickAwayControllerSpec.js deleted file mode 100644 index 96e7b6c13f..0000000000 --- a/platform/search/test/controllers/ClickAwayControllerSpec.js +++ /dev/null @@ -1,94 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web 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 Web 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. - *****************************************************************************/ -/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ - -define( - ["../../src/controllers/ClickAwayController"], - function (ClickAwayController) { - "use strict"; - - describe("The click-away controller", function () { - var mockScope, - mockDocument, - controller; - - beforeEach(function () { - mockScope = jasmine.createSpyObj( - "$scope", - [ "$apply" ] - ); - mockDocument = jasmine.createSpyObj( - "$document", - [ "on", "off" ] - ); - controller = new ClickAwayController(mockScope, mockDocument); - }); - - it("is initially inactive", function () { - expect(controller.isActive()).toBe(false); - }); - - it("does not listen to the document before being toggled", function () { - expect(mockDocument.on).not.toHaveBeenCalled(); - }); - - it("tracks enabled/disabled state when toggled", function () { - controller.toggle(); - expect(controller.isActive()).toBe(true); - controller.toggle(); - expect(controller.isActive()).toBe(false); - controller.toggle(); - expect(controller.isActive()).toBe(true); - controller.toggle(); - expect(controller.isActive()).toBe(false); - }); - - it("allows active state to be explictly specified", function () { - controller.setState(true); - expect(controller.isActive()).toBe(true); - controller.setState(true); - expect(controller.isActive()).toBe(true); - controller.setState(false); - expect(controller.isActive()).toBe(false); - controller.setState(false); - expect(controller.isActive()).toBe(false); - }); - - it("registers a mouse listener when activated", function () { - controller.setState(true); - expect(mockDocument.on).toHaveBeenCalled(); - }); - - it("deactivates and detaches listener on document click", function () { - var callback; - controller.setState(true); - callback = mockDocument.on.mostRecentCall.args[1]; - callback(); - expect(controller.isActive()).toEqual(false); - expect(mockDocument.off).toHaveBeenCalledWith("mouseup", callback); - }); - - - - }); - } -); \ No newline at end of file From 00433f02bccd7c43b7e91ec0b010ac24ccb9362b Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 5 May 2016 21:42:53 -0700 Subject: [PATCH 37/38] #892 Removed browser warning --- platform/commonUI/general/bundle.js | 9 -- .../general/src/UnsupportedBrowserWarning.js | 64 ------------ .../test/UnsupportedBrowserWarningSpec.js | 98 ------------------- 3 files changed, 171 deletions(-) delete mode 100644 platform/commonUI/general/src/UnsupportedBrowserWarning.js delete mode 100644 platform/commonUI/general/test/UnsupportedBrowserWarningSpec.js diff --git a/platform/commonUI/general/bundle.js b/platform/commonUI/general/bundle.js index 3e7f558e8b..8b34b0a897 100644 --- a/platform/commonUI/general/bundle.js +++ b/platform/commonUI/general/bundle.js @@ -26,7 +26,6 @@ define([ "./src/services/PopupService", "./src/SplashScreenManager", "./src/StyleSheetLoader", - "./src/UnsupportedBrowserWarning", "./src/controllers/TimeRangeController", "./src/controllers/DateTimePickerController", "./src/controllers/DateTimeFieldController", @@ -75,7 +74,6 @@ define([ PopupService, SplashScreenManager, StyleSheetLoader, - UnsupportedBrowserWarning, TimeRangeController, DateTimePickerController, DateTimeFieldController, @@ -153,13 +151,6 @@ define([ "THEME" ] }, - { - "implementation": UnsupportedBrowserWarning, - "depends": [ - "notificationService", - "agentService" - ] - }, { "implementation": SplashScreenManager, "depends": [ diff --git a/platform/commonUI/general/src/UnsupportedBrowserWarning.js b/platform/commonUI/general/src/UnsupportedBrowserWarning.js deleted file mode 100644 index f2fa0c3f20..0000000000 --- a/platform/commonUI/general/src/UnsupportedBrowserWarning.js +++ /dev/null @@ -1,64 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web 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 Web 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. - *****************************************************************************/ -/*global define*/ - -/** - * This bundle provides various general-purpose UI elements, including - * platform styling. - * @namespace platform/commonUI/general - */ -define( - [], - function () { - "use strict"; - - var WARNING_TITLE = "Unsupported browser", - WARNING_DESCRIPTION = [ - "This software has been developed and tested", - "using the latest Google Chrome,", - "and may be unstable in other browsers." - ].join(" "), - MOBILE_BROWSER = "Safari", - DESKTOP_BROWSER = "Chrome"; - - /** - * Shows a warning if a user's browser is unsupported. - * @memberof platform/commonUI/general - * @constructor - * @param {NotificationService} notificationService the notification - * service - */ - function UnsupportedBrowserWarning(notificationService, agentService) { - var testToBrowser = agentService.isMobile() ? - MOBILE_BROWSER : DESKTOP_BROWSER; - - if (!agentService.isBrowser(testToBrowser)) { - notificationService.alert({ - title: WARNING_TITLE, - actionText: WARNING_DESCRIPTION - }); - } - } - - return UnsupportedBrowserWarning; - } -); diff --git a/platform/commonUI/general/test/UnsupportedBrowserWarningSpec.js b/platform/commonUI/general/test/UnsupportedBrowserWarningSpec.js deleted file mode 100644 index 507a92c62f..0000000000 --- a/platform/commonUI/general/test/UnsupportedBrowserWarningSpec.js +++ /dev/null @@ -1,98 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web 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 Web 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. - *****************************************************************************/ -/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ - -define( - ["../src/UnsupportedBrowserWarning"], - function (UnsupportedBrowserWarning) { - "use strict"; - - var MOBILE_BROWSER = "Safari", - DESKTOP_BROWSER = "Chrome", - UNSUPPORTED_BROWSERS = [ - "Firefox", - "IE", - "Opera", - "Iceweasel" - ]; - - describe("The unsupported browser warning", function () { - var mockNotificationService, - mockAgentService, - testAgent; - - function instantiateWith(browser) { - testAgent = "Mozilla/5.0 " + browser + "/12.34.56"; - return new UnsupportedBrowserWarning( - mockNotificationService, - mockAgentService - ); - } - - beforeEach(function () { - testAgent = "chrome"; - mockNotificationService = jasmine.createSpyObj( - "notificationService", - [ "alert" ] - ); - mockAgentService = jasmine.createSpyObj( - "agentService", - [ "isMobile", "isBrowser" ] - ); - mockAgentService.isBrowser.andCallFake(function (substr) { - substr = substr.toLowerCase(); - return testAgent.toLowerCase().indexOf(substr) !== -1; - }); - }); - - [ false, true ].forEach(function (isMobile) { - var deviceType = isMobile ? "mobile" : "desktop", - goodBrowser = isMobile ? MOBILE_BROWSER : DESKTOP_BROWSER, - badBrowsers = UNSUPPORTED_BROWSERS.concat([ - isMobile ? DESKTOP_BROWSER : MOBILE_BROWSER - ]); - - describe("on " + deviceType + " devices", function () { - beforeEach(function () { - mockAgentService.isMobile.andReturn(isMobile); - }); - - it("is not shown for " + goodBrowser, function () { - instantiateWith(goodBrowser); - expect(mockNotificationService.alert) - .not.toHaveBeenCalled(); - }); - - badBrowsers.forEach(function (badBrowser) { - it("is shown for " + badBrowser, function () { - instantiateWith(badBrowser); - expect(mockNotificationService.alert) - .toHaveBeenCalled(); - }); - }); - }); - }); - - }); - } -); - From 1cdeccc7a7186901b7ed3356bf4f7c28113f1f84 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 5 May 2016 21:28:41 -0700 Subject: [PATCH 38/38] #885 added command line option --directory -D to specify base directory --- app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 90c30c9e26..8e7bb15ec2 100644 --- a/app.js +++ b/app.js @@ -19,6 +19,7 @@ // Defaults options.port = options.port || options.p || 8080; + options.directory = options.directory || options.D || '.'; ['include', 'exclude', 'i', 'x'].forEach(function (opt) { options[opt] = options[opt] || []; // Make sure includes/excludes always end up as arrays @@ -36,6 +37,7 @@ console.log(" --port, -p Specify port."); console.log(" --include, -i Include the specified bundle."); console.log(" --exclude, -x Exclude the specified bundle."); + console.log(" --directory, -D Serve files from specified directory."); console.log(""); process.exit(0); } @@ -71,7 +73,7 @@ }); // Expose everything else as static files - app.use(express['static']('.')); + app.use(express['static'](options.directory)); // Finally, open the HTTP server app.listen(options.port);