Compare commits

..

1 Commits

Author SHA1 Message Date
Henry
770a5b0df8 Implementation of summary widgets. Fixes #1644 2017-10-25 11:24:06 -07:00
130 changed files with 2032 additions and 4177 deletions

View File

@@ -21,6 +21,5 @@
"shadow": "outer",
"strict": "implied",
"undef": true,
"unused": "vars",
"latedef": "nofunc"
"unused": "vars"
}

15
API.md
View File

@@ -879,21 +879,6 @@ openmct.install(openmct.plugins.CouchDB('http://localhost:9200'))
* `openmct.plugins.Espresso` and `openmct.plugins.Snow` are two different
themes (dark and light) available for Open MCT. Note that at least one
of these themes must be installed for Open MCT to appear correctly.
* `openmct.plugins.URLIndicatorPlugin` adds an indicator which shows the
availability of a URL with the following options:
- `url` : URL to indicate the status of
- `cssClass`: Icon to show in the status bar, defaults to `icon-database`, [list of all icons](https://nasa.github.io/openmct/style-guide/#/browse/styleguide:home?view=items)
- `interval`: Interval between checking the connection, defaults to `10000`
- `label` Name showing up as text in the status bar, defaults to url
```javascript
openmct.install(openmct.plugins.URLIndicatorPlugin({
url: 'http://google.com',
cssClass: 'check',
interval: 10000,
label: 'Google'
})
);
```
* `openmct.plugins.LocalStorage` provides persistence of user-created
objects in browser-local storage. This is particularly useful in
development environments.

View File

@@ -88,7 +88,7 @@ and [`gulp`](http://gulpjs.com/).
To build Open MCT for deployment:
`npm run prepare`
`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

View File

@@ -17,8 +17,8 @@
"screenfull": "^3.0.0",
"node-uuid": "^1.4.7",
"comma-separated-values": "^3.6.4",
"file-saver": "^1.3.3",
"zepto": "^1.1.6",
"FileSaver.js": "^0.0.2",
"zepto": "1.2.0",
"eventemitter3": "^1.2.0",
"lodash": "3.10.1",
"almond": "~0.3.2",

View File

@@ -1,11 +1,3 @@
machine:
node:
version: 4.7.0
dependencies:
pre:
- npm install -g npm@latest
deployment:
production:
branch: master
@@ -24,4 +16,4 @@ test:
general:
branches:
ignore:
- gh-pages
- gh-pages

View File

@@ -2283,7 +2283,7 @@ To install build dependencies (only needs to be run once):
To build:
`npm run prepare`
`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

View File

@@ -44,7 +44,9 @@ define([
message = message.data;
var callback = this.callbacks[message.id];
if (callback) {
callback(message);
if (callback(message)) {
delete this.callbacks[message.id];
}
}
};
@@ -70,7 +72,6 @@ define([
deferred.resolve = resolve;
deferred.reject = reject;
});
var messageId;
function callback(message) {
if (message.error) {
@@ -78,27 +79,33 @@ define([
} else {
deferred.resolve(message.data);
}
delete this.callbacks[messageId];
return true;
}
messageId = this.dispatch('request', request, callback.bind(this));
this.dispatch('request', request, callback);
return promise;
};
WorkerInterface.prototype.subscribe = function (request, cb) {
function callback(message) {
var isCancelled = false;
var callback = function (message) {
if (isCancelled) {
return true;
}
cb(message.data);
};
var messageId = this.dispatch('subscribe', request, callback);
var messageId = this.dispatch('subscribe', request, callback)
return function () {
isCancelled = true;
this.dispatch('unsubscribe', {
id: messageId
});
delete this.callbacks[messageId];
}.bind(this);
};

View File

@@ -1,9 +1,9 @@
<span class="l-indicator s-indicator icon-box-with-arrow" ng-controller="DialogLaunchController">
<span class="status block" ng-controller="DialogLaunchController">
<!-- DO NOT ADD SPACES BETWEEN THE SPANS - IT ADDS WHITE SPACE!! -->
<span class="label">
<span class="status-indicator icon-box-with-arrow"></span><span class="label">
<a ng-click="launchProgress(true)">Known</a> |
<a ng-click="launchProgress(false)">Unknown</a> |
<a ng-click="launchError()">Error</a> |
<a ng-click="launchInfo()">Info</a>
</span>
</span><span class="count"></span>
</span>

View File

@@ -1,9 +1,9 @@
<span class="l-indicator s-indicator icon-bell" ng-controller="NotificationLaunchController">
<span class="status block" ng-controller="NotificationLaunchController">
<!-- DO NOT ADD SPACES BETWEEN THE SPANS - IT ADDS WHITE SPACE!! -->
<span class="label">
<span class="status-indicator icon-bell"></span><span class="label">
<a ng-click="newInfo()">Success</a> |
<a ng-click="newError()">Error</a> |
<a ng-click="newAlert()">Alert</a> |
<a ng-click="newProgress()">Progress</a>
</span>
</span><span class="count"></span>
</span>

View File

@@ -129,44 +129,6 @@
</div>
</div>
<div class="l-section">
<h2>Status Bar Indicators</h2>
<div class="cols cols1-1">
<div class="col">
<p>Indicators content goes here.</p>
</div>
<mct-example><div class="status-holder s-status-bar">
<span class="status-block-holder">
<span class="l-indicator s-indicator">
<span class="status-indicator icon-bell"></span>
<span class="label">Collapsible Indicator</span>
</span>
</span>
<span class="status-block-holder no-collapse caution">
<span class="l-indicator s-indicator">
<span class="status-indicator icon-bell"></span>
<span class="label">No collapse</span>
</span>
</span>
</div>
<div class="status-holder s-status-bar">
<span class="status-block-holder">
<span class="l-indicator s-indicator">
<span class="status-indicator icon-bell"></span>
<span class="label">Collapsible Indicator</span>
</span>
</span>
<span class="status-block-holder">
<span class="status block s-status-info icon-info">
<span class="label">No collapse</span>
</span>
</span>
</div>
</mct-example>
</div>
</div>
<div class="l-section">
<h2>Synchronization</h2>
<div class="cols cols1-1">

View File

@@ -22,8 +22,6 @@
/*global require,__dirname*/
require("v8-compile-cache");
var gulp = require('gulp'),
sourcemaps = require('gulp-sourcemaps'),
path = require('path'),
@@ -179,4 +177,4 @@ gulp.task('install', [ 'assets', 'scripts' ]);
gulp.task('verify', [ 'lint', 'test', 'checkstyle' ]);
gulp.task('build', [ 'verify', 'install' ]);
gulp.task('build', [ 'verify', 'install' ]);

View File

@@ -43,9 +43,6 @@
openmct.install(openmct.plugins.ExampleImagery());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(openmct.plugins.ImportExport());
openmct.install(openmct.plugins.AutoflowView({
type: "telemetry.panel"
}));
openmct.install(openmct.plugins.Conductor({
menuOptions: [
{

View File

@@ -36,7 +36,6 @@ module.exports = function(config) {
files: [
{pattern: 'bower_components/**/*.js', included: false},
{pattern: 'node_modules/d3-*/**/*.js', included: false},
{pattern: 'node_modules/vue/**/*.js', included: false},
{pattern: 'src/**/*.js', included: false},
{pattern: 'example/**/*.html', included: false},
{pattern: 'example/**/*.js', included: false},

View File

@@ -33,11 +33,10 @@ requirejs.config({
"moment": "bower_components/moment/moment",
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
"moment-timezone": "bower_components/moment-timezone/builds/moment-timezone-with-data",
"saveAs": "bower_components/file-saver/FileSaver.min",
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
"screenfull": "bower_components/screenfull/dist/screenfull.min",
"text": "bower_components/text/text",
"uuid": "bower_components/node-uuid/uuid",
"vue": "node_modules/vue/dist/vue.min",
"zepto": "bower_components/zepto/zepto.min",
"lodash": "bower_components/lodash/lodash",
"d3-selection": "node_modules/d3-selection/build/d3-selection.min",
@@ -67,9 +66,6 @@ requirejs.config({
"moment-duration-format": {
"deps": ["moment"]
},
"saveAs": {
"exports": "saveAs"
},
"screenfull": {
"exports": "screenfull"
},

View File

@@ -15,8 +15,7 @@
"d3-time-format": "^2.0.3",
"express": "^4.13.1",
"minimist": "^1.1.1",
"request": "^2.69.0",
"vue": "^2.5.6"
"request": "^2.69.0"
},
"devDependencies": {
"bower": "^1.7.7",
@@ -50,8 +49,7 @@
"moment": "^2.11.1",
"node-bourbon": "^4.2.3",
"requirejs": "2.1.x",
"split": "^1.0.0",
"v8-compile-cache": "^1.1.0"
"split": "^1.0.0"
},
"scripts": {
"start": "node app.js",
@@ -61,7 +59,7 @@
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
"otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
"docs": "npm run jsdoc ; npm run otherdoc",
"prepare": "node ./node_modules/bower/bin/bower install && node ./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",

View File

@@ -57,12 +57,7 @@
</div>
<mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll"
mct-selectable="{
item: domainObject.useCapability('adapter'),
oldItem: domainObject
}"
mct-init-select>
class="abs flex-elem grows object-holder-main scroll">
</mct-representation>
</div>
</div>

View File

@@ -19,21 +19,12 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="InspectorController as controller">
<div ng-controller="InspectorController">
<div ng-repeat="region in regions">
<mct-representation
key="'object-properties'"
mct-object="controller.selectedItem()"
key="region.content.key"
mct-object="domainObject"
ng-model="ngModel">
</mct-representation>
<div ng-if="!controller.hasProviderView()">
<mct-representation
key="inspectorKey"
mct-object="controller.selectedItem()"
ng-model="ngModel">
</mct-representation>
</div>
<div class='inspector-provider-view'>
</div>
</div>
</div>

View File

@@ -38,6 +38,8 @@
ng-class="{ last:($index + 1) === contextualParents.length }">
<mct-representation key="'label'"
mct-object="parent"
ng-model="ngModel"
ng-click="ngModel.selectedObject = parent"
class="location-item">
</mct-representation>
</span>
@@ -49,6 +51,8 @@
ng-class="{ last:($index + 1) === primaryParents.length }">
<mct-representation key="'label'"
mct-object="parent"
ng-model="ngModel"
ng-click="ngModel.selectedObject = parent"
class="location-item">
</mct-representation>
</span>

View File

@@ -121,8 +121,7 @@ define([
"key": "ElementsController",
"implementation": ElementsController,
"depends": [
"$scope",
"openmct"
"$scope"
]
},
{
@@ -300,6 +299,9 @@ define([
{
"key": "edit-elements",
"template": elementsTemplate,
"uses": [
"composition"
],
"gestures": [
"drop"
]
@@ -383,10 +385,7 @@ define([
]
},
{
"implementation": EditToolbarRepresenter,
"depends": [
"openmct"
]
"implementation": EditToolbarRepresenter
}
],
"constants": [

View File

@@ -61,12 +61,7 @@
<mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll"
toolbar="toolbar"
mct-selectable="{
item: domainObject.useCapability('adapter'),
oldItem: domainObject
}"
mct-init-select>
toolbar="toolbar">
</mct-representation>
</div><!--/ l-object-wrapper-inner -->
</div>

View File

@@ -25,7 +25,7 @@
ng-model="filterBy">
</mct-include>
<div class="flex-elem grows vscroll">
<ul class="tree" ng-if="composition.length > 0">
<ul class="tree">
<li ng-repeat="containedObject in composition | filter:searchElements">
<span class="tree-item">
<mct-representation
@@ -36,6 +36,5 @@
</span>
</li>
</ul>
<div ng-if="composition.length === 0">No contained elements</div>
</div>
</div>

View File

@@ -101,15 +101,10 @@ define(
*/
EditorCapability.prototype.finish = function () {
var domainObject = this.domainObject;
if (this.transactionService.isActive()) {
return this.transactionService.cancel().then(function () {
domainObject.getCapability("status").set("editing", false);
return domainObject;
});
} else {
return Promise.resolve(domainObject);
}
return this.transactionService.cancel().then(function () {
domainObject.getCapability("status").set("editing", false);
return domainObject;
});
};
/**

View File

@@ -28,6 +28,16 @@ define(
[],
function () {
function isDirty(domainObject) {
var navigatedObject = domainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor");
return editorCapability &&
editorCapability.isEditContextRoot() &&
editorCapability.dirty();
}
function cancelEditing(domainObject) {
var navigatedObject = domainObject,
editorCapability = navigatedObject &&
@@ -49,7 +59,10 @@ define(
var removeCheck = navigationService
.checkBeforeNavigation(function () {
return "Continuing will cause the loss of any unsaved changes.";
if (isDirty(domainObject)) {
return "Continuing will cause the loss of any unsaved changes.";
}
return false;
});
$scope.$on('$destroy', function () {

View File

@@ -29,11 +29,7 @@ define(
*
* @constructor
*/
function ElementsController($scope, openmct) {
this.scope = $scope;
this.scope.composition = [];
var self = this;
function ElementsController($scope) {
function filterBy(text) {
if (typeof text === 'undefined') {
return $scope.searchText;
@@ -51,45 +47,10 @@ define(
}
}
function setSelection(selection) {
self.scope.selection = selection;
self.refreshComposition(selection);
}
$scope.filterBy = filterBy;
$scope.searchElements = searchElements;
openmct.selection.on('change', setSelection);
setSelection(openmct.selection.get());
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
});
}
/**
* Gets the composition for the selected object and populates the scope with it.
*
* @param selection the selection object
* @private
*/
ElementsController.prototype.refreshComposition = function (selection) {
if (!selection[0]) {
return;
}
var selected = selection[0].context.oldItem;
var selectedObjectComposition = selected && selected.useCapability('composition');
if (selectedObjectComposition) {
selectedObjectComposition.then(function (composition) {
this.scope.composition = composition;
}.bind(this));
} else {
this.scope.composition = [];
}
};
return ElementsController;
}
);

View File

@@ -38,7 +38,7 @@ define(
* @constructor
* @implements {Representer}
*/
function EditToolbarRepresenter(openmct, scope, element, attrs) {
function EditToolbarRepresenter(scope, element, attrs) {
var self = this;
// Mark changes as ready to persist
@@ -109,7 +109,6 @@ define(
this.updateSelection = updateSelection;
this.toolbar = undefined;
this.toolbarObject = {};
this.openmct = openmct;
// If this representation exposes a toolbar, set up watches
// to synchronize with it.
@@ -147,7 +146,7 @@ define(
// Expose the toolbar object to the parent scope
initialize(definition);
// Create a selection scope
this.setSelection(new EditToolbarSelection(this.openmct));
this.setSelection(new EditToolbarSelection());
// Initialize toolbar to an empty selection
this.updateSelection([]);
};

View File

@@ -38,24 +38,10 @@ define(
* @memberof platform/commonUI/edit
* @constructor
*/
function EditToolbarSelection(openmct) {
function EditToolbarSelection() {
this.selection = [{}];
this.selecting = false;
this.selectedObj = undefined;
openmct.selection.on('change', function (selection) {
var selected = selection[0];
if (selected && selected.context.toolbar) {
this.select(selected.context.toolbar);
} else {
this.deselect();
}
if (selected && selected.context.viewProxy) {
this.proxy(selected.context.viewProxy);
}
}.bind(this));
}
/**

View File

@@ -62,7 +62,6 @@ define(
);
mockTransactionService.commit.andReturn(fastPromise());
mockTransactionService.cancel.andReturn(fastPromise());
mockTransactionService.isActive = jasmine.createSpy('isActive');
mockStatusCapability = jasmine.createSpyObj(
"statusCapability",
@@ -142,7 +141,6 @@ define(
describe("finish", function () {
beforeEach(function () {
mockTransactionService.isActive.andReturn(true);
capability.edit();
capability.finish();
});
@@ -154,23 +152,6 @@ define(
});
});
describe("finish", function () {
beforeEach(function () {
mockTransactionService.isActive.andReturn(false);
capability.edit();
});
it("does not cancel transaction when transaction is not active", function () {
capability.finish();
expect(mockTransactionService.cancel).not.toHaveBeenCalled();
});
it("returns a promise", function () {
expect(capability.finish() instanceof Promise).toBe(true);
});
});
describe("dirty", function () {
var model = {};

View File

@@ -104,10 +104,10 @@ define(
mockEditorCapability.isEditContextRoot.andReturn(false);
mockEditorCapability.dirty.andReturn(false);
expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes.");
expect(checkFn()).toBe(false);
mockEditorCapability.isEditContextRoot.andReturn(true);
expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes.");
expect(checkFn()).toBe(false);
mockEditorCapability.dirty.andReturn(true);
expect(checkFn())

View File

@@ -27,23 +27,11 @@ define(
describe("The Elements Pane controller", function () {
var mockScope,
mockOpenMCT,
mockSelection,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", ['$on']);
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
controller = new ElementsController(mockScope, mockOpenMCT);
mockScope = jasmine.createSpy("$scope");
controller = new ElementsController(mockScope);
});
function getModel(model) {

View File

@@ -29,9 +29,7 @@ define(
mockElement,
testAttrs,
mockUnwatch,
representer,
mockOpenMCT,
mockSelection;
representer;
beforeEach(function () {
mockScope = jasmine.createSpyObj(
@@ -48,18 +46,7 @@ define(
mockScope.$parent.$watchCollection.andReturn(mockUnwatch);
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
representer = new EditToolbarRepresenter(
mockOpenMCT,
mockScope,
mockElement,
testAttrs

View File

@@ -28,25 +28,13 @@ define(
var testProxy,
testElement,
otherElement,
selection,
mockSelection,
mockOpenMCT;
selection;
beforeEach(function () {
testProxy = { someKey: "some value" };
testElement = { someOtherKey: "some other value" };
otherElement = { yetAnotherKey: 42 };
mockSelection = jasmine.createSpyObj("selection", [
// 'select',
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
selection = new EditToolbarSelection(mockOpenMCT);
selection = new EditToolbarSelection();
selection.proxy(testProxy);
});

View File

@@ -121,9 +121,6 @@ define([
};
UTCTimeFormat.prototype.parse = function (text) {
if (typeof text === 'number') {
return text;
}
return moment.utc(text, DATE_FORMATS).valueOf();
};

View File

@@ -41,7 +41,6 @@ define([
"./src/controllers/BannerController",
"./src/directives/MCTContainer",
"./src/directives/MCTDrag",
"./src/directives/MCTSelectable",
"./src/directives/MCTClickElsewhere",
"./src/directives/MCTResize",
"./src/directives/MCTPopup",
@@ -91,7 +90,6 @@ define([
BannerController,
MCTContainer,
MCTDrag,
MCTSelectable,
MCTClickElsewhere,
MCTResize,
MCTPopup,
@@ -330,13 +328,6 @@ define([
"$document"
]
},
{
"key": "mctSelectable",
"implementation": MCTSelectable,
"depends": [
"openmct"
]
},
{
"key": "mctClickElsewhere",
"implementation": MCTClickElsewhere,

View File

@@ -25,7 +25,6 @@
}
.l-fixed-position-item {
border-width: 1px;
position: absolute;
&.s-not-selected {
opacity: 0.8;

View File

@@ -42,7 +42,6 @@
@import "controls/lists";
@import "controls/menus";
@import "controls/messages";
@import "controls/indicators";
@import "mobile/controls/menus";
/********************************* FORMS */

View File

@@ -20,15 +20,13 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*************************************************** MIXINS */
@mixin elementStatusColors($c) {
@mixin formulateStatusColors($c) {
// Sets bg and icon colors for elements
background: rgba($c, 0.4) !important;
&:before { color: $c !important; }
}
@mixin indicatorStatusColors($c) {
&:before, .count { color: $c; }
}
/*************************************************** GENERAL */
.s-limit-yellow,
@@ -56,13 +54,14 @@
}
}
/*************************************************** LIMITS */
.s-limit-yellow, .s-limit-yellow-icon {
@include elementStatusColors($colorWarningLo);
@include formulateStatusColors($colorWarningLo);
}
.s-limit-red, .s-limit-red-icon {
@include elementStatusColors($colorWarningHi);
@include formulateStatusColors($colorWarningHi);
}
.s-limit-upr:before { content: $glyph-icon-arrow-double-up; }
@@ -71,11 +70,11 @@
.s-limit-red-icon:before { content: $glyph-icon-alert-triangle; }
/*************************************************** STATUS */
.s-status-warning-hi, .s-status-warning-hi-icon { @include elementStatusColors($colorWarningHi); }
.s-status-warning-lo, .s-status-warning-lo-icon { @include elementStatusColors($colorWarningLo); }
.s-status-diagnostic, .s-status-diagnostic-icon { @include elementStatusColors($colorDiagnostic); }
.s-status-info, .s-status-info-icon { @include elementStatusColors($colorInfo); }
.s-status-ok, .s-status-ok-icon { @include elementStatusColors($colorOk); }
.s-status-warning-hi, .s-status-warning-hi-icon { @include formulateStatusColors($colorWarningHi); }
.s-status-warning-lo, .s-status-warning-lo-icon { @include formulateStatusColors($colorWarningLo); }
.s-status-diagnostic, .s-status-diagnostic-icon { @include formulateStatusColors($colorDiagnostic); }
.s-status-info, .s-status-info-icon { @include formulateStatusColors($colorInfo); }
.s-status-ok, .s-status-ok-icon { @include formulateStatusColors($colorOk); }
.s-status-warning-hi-icon:before { content: $glyph-icon-alert-triangle; }
.s-status-warning-lo-icon:before { content: $glyph-icon-alert-rect; }
@@ -83,25 +82,4 @@
.s-status-info-icon:before { content: $glyph-icon-info; }
.s-status-ok-icon:before { content: $glyph-icon-check; }
/*************************************************** INDICATORS */
.s-indicator-status-info {
@include indicatorStatusColors($colorInfo);
}
.s-indicator-status-ok {
@include indicatorStatusColors($colorOk);
}
.s-indicator-status-caution, .s-indicator-status-warning, .s-indicator-status-alert {
@include indicatorStatusColors($colorStatusAlert);
}
.s-indicator-status-error, .s-indicator-status-err {
@include indicatorStatusColors($colorStatusError);
}
.s-indicator-status-available {
@include indicatorStatusColors($colorStatusAvailable);
}
.s-indicator-status-subdued {
opacity: 0.7;
}

View File

@@ -1,99 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* Indicators are generally only displayed in the ue-bottom-bar element of the main interface */
.l-indicator {
$transDelayHover: 1.5s;
$transDelayWhileExpanded: 2.5s;
$transSpeed: .25s;
display: inline-block;
margin-right: $interiorMarginSm;
&:before {
// Icon
display: inline-block;
opacity: 0.85;
}
.label,
.count {
display: inline-block;
vertical-align: top;
}
&.float-right {
float: right;
}
&.no-icon {
&:before {
display: none;
}
}
&:not(.no-collapse) {
.label {
// Max-width silliness is necessary for width transition
@include trans-prop-nice((max-width, margin-right), $transSpeed, $transDelayWhileExpanded);
overflow: hidden;
margin-right: 0;
max-width: 0px;
white-space: nowrap;
}
&:hover {
&:before {
opacity: 1;
}
.label {
@include trans-prop-nice((max-width, margin-right), 0s);
margin-right: $interiorMargin;
max-width: 600px;
width: auto;
}
.count {
@include trans-prop-nice(max-width, 0s);
opacity: 0;
}
}
}
.count {
@include trans-prop-nice(opacity, $transSpeed, $transDelayWhileExpanded);
font-weight: bold;
opacity: 1;
}
.s-button {
background: $colorStatusBtnBg;
padding: 0 $interiorMargin;
height: auto;
line-height: inherit;
}
}
.s-indicator {
color: $colorStatusDefault;
}

View File

@@ -36,6 +36,110 @@
}
}
// Status coloring
.ok, .info {
.status-indicator {
color: $colorInfo;
}
}
.alert, .caution, .warning {
.status-indicator, .count {
color: $colorStatusAlert;
}
}
.error, .err {
.status-indicator, .count {
color: $colorStatusError;
}
}
.available {
.status-indicator, .count {
color: $colorStatusAvailable;
}
}
.subdued {
.status-indicator {
color: pullForward($colorStatusBarBg, 40%);
}
}
.status-block-holder {
// Applied to mct-include element
// Contains status.block elements
$transDelay: 1.5s;
$transSpeed: .25s;
display: inline-block;
&.clickable { cursor: pointer; }
&:not(.clickable) { cursor: default; }
&.no-icon .status.block {
.status-indicator {
display: none;
}
}
&.float-right {
float: right;
}
&:not(.no-collapse) .status.block {
.label {
// Max-width silliness is necessary for width transition
@include trans-prop-nice(max-width, $transSpeed, $transDelay);
overflow: hidden;
max-width: 0px;
}
&:hover {
.label {
@include trans-prop-nice(max-width, $transSpeed, 0s);
max-width: 600px;
width: auto;
}
.count {
@include trans-prop-nice(max-width, $transSpeed, 0s);
opacity: 0;
}
}
}
}
.status.block {
$transDelay: 1.5s;
$transSpeed: .25s;
color: $colorStatusDefault;
display: inline-block;
margin-right: $interiorMargin;
.status-indicator,
.label,
.count {
display: inline-block;
vertical-align: top;
}
.status-indicator {
background: none !important;
margin-right: $interiorMarginSm;
&[class*='s-status']:before {
font-size: 1em;
}
}
.count {
@include trans-prop-nice(opacity, $transSpeed, $transDelay);
font-weight: bold;
opacity: 1;
}
.s-button {
background: $colorStatusBtnBg;
padding: 0 $interiorMargin;
height: auto;
line-height: inherit;
}
}
/******************************************************************* MESSAGE BANNERS */
.message {
&.block {

View File

@@ -80,32 +80,23 @@
// Editing Grids
.l-grid-holder {
display: block;
.l-grid {
&.l-grid-x { @include bgTicks($colorGridLines, 'x'); }
&.l-grid-y { @include bgTicks($colorGridLines, 'y'); }
}
}
// Display grid when selected or selection parent.
.s-selected .l-grid-holder,
.s-selected-parent .l-grid-holder {
display: block;
// Prevent nested frames from showing their grids
.t-frame-outer .l-grid-holder { display: none !important; }
// Prevent nested elements from showing s-hover-border
.t-frame-outer .s-hover-border {
border: none !important;
}
// Display in nested frames...
.t-frame-outer {
// ...when drilled in or selection parent...
&.s-drilled-in, &.s-selected-parent {
.l-grid-holder {
display: block;
}
.t-frame-outer:not(.s-drilled-in) .l-grid-holder {
display: none;
}
}
// ...but hide otherwise.
.l-grid-holder {
display: none;
}
// Prevent nested frames from being selectable until we have proper sub-object editing
.t-frame-outer .t-frame-outer {
pointer-events: none;
}
}

View File

@@ -26,10 +26,12 @@
z-index: 0; // Needed to prevent child-frame controls from showing through when another child-frame is above
&:not(.no-frame) {
background: $colorBodyBg;
border-color: $bc;
border: 1px solid $bc;
&:hover {
border-color: lighten($bc, 10%);
}
}
}
.object-browse-bar {
font-size: 0.75em;
height: $ohH;
@@ -90,9 +92,9 @@
&.no-frame {
background: transparent !important;
border-color: transparent;
border: none !important;
.object-browse-bar .right {
$m: 0;
$m: 0; // $interiorMarginSm;
background: rgba(black, 0.3);
border-radius: $basicCr;
padding: $interiorMarginSm;
@@ -102,7 +104,7 @@
}
&.t-frame-outer > .t-rep-frame {
&.contents {
$m: 0px;
$m: 2px;
top: $m;
right: $m;
bottom: $m;
@@ -113,7 +115,6 @@
display: none;
}
> .object-holder.abs {
overflow: hidden;
top: 0 !important;
}
}

View File

@@ -81,11 +81,16 @@
@include absPosDefault(0);// New status bar design
top: auto;
height: $ueFooterH;
.indicator-holder {
line-height: $ueFooterH - ($interiorMargin * 2);
background: $colorStatusBarBg;
color: lighten($colorBodyBg, 30%);
font-size: .7rem;
.status-holder {
box-sizing: border-box;
@include absPosDefault($interiorMargin);
@include ellipsize();
right: 120px;
text-transform: uppercase;
z-index: 1;
}
.app-logo {
@@ -100,14 +105,6 @@
}
}
.s-status-bar {
background: $colorStatusBarBg;
color: lighten($colorBodyBg, 30%);
font-size: .7rem;
line-height: $ueFooterH - ($interiorMargin * 2);
text-transform: uppercase;
}
.edit-mode {
// Old edit mode
.split-layout {

View File

@@ -20,52 +20,35 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
.s-hover-border {
border: 1px solid transparent;
&:hover {
border-color: rgba($colorSelectableSelectedPrimary, 0.5) !important;
}
border: 1px dotted transparent;
}
.s-status-editing {
// Limit to editing mode
$o: 0.5;
$oHover: 0.8;
$bc: $colorSelectableSelectedPrimary;
// Limit to editing mode until we have sub-object selection
.s-hover-border {
// Show a border by default so user can see object bounds and empty objects
border-color: rgba($bc, $o) !important;
border-style: dotted !important;
border: 1px dotted rgba($colorSelectableSelectedPrimary, 0.3) !important;
&:hover {
border-color: rgba($bc, $oHover) !important;
}
&.t-object-type-layout {
border-style: dashed !important;
border-color: rgba($colorSelectableSelectedPrimary, 0.7) !important;
}
}
.s-selected {
&.s-moveable {
&:not(.s-drilled-in) {
cursor: move;
.s-selected > .s-hover-border,
.s-selected.s-hover-border {
// Styles for a selected object. Also used by legacy Fixed Position/Panel objects.
border-color: $colorSelectableSelectedPrimary !important;
@include boxShdwLarge();
// Show edit-corners if you got 'em
.edit-corner {
display: block;
&:hover {
background-color: rgba($colorKey, 1);
}
}
}
}
.s-selected > .s-hover-border,
.s-selected.s-hover-border {
// Styles for a selected object. Also used by legacy Fixed Position/Panel objects.
border-color: $colorSelectableSelectedPrimary !important;
@include boxShdwLarge();
// Show edit-corners if you got 'em
.edit-corner {
display: block;
&:hover {
background-color: rgba($colorKey, 1);
}
.s-selected > .s-moveable,
.s-selected.s-moveable {
cursor: move;
}
}
}

View File

@@ -19,11 +19,13 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class='abs bottom-bar ue-bottom-bar s-status-bar mobile-disable-select' ng-controller="BottomBarController as bar">
<div id='status' class='indicator-holder'>
<div class='abs bottom-bar ue-bottom-bar mobile-disable-select' ng-controller="BottomBarController as bar">
<div id='status' class='status-holder'>
<mct-include ng-repeat="indicator in bar.getIndicators()"
ng-model="indicator.ngModel"
key="indicator.template">
key="indicator.template"
class="status-block-holder"
ng-class='indicator.ngModel.getGlyphClass()'>
</mct-include>
</div>
<mct-include key="'message-banner'"></mct-include>

View File

@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<span ng-controller="DateTimeFieldController">
<input type="text" autocorrect="off" spellcheck="false"
<input type="text"
ng-model="textValue"
ng-blur="restoreTextValue(); ngBlur()"
ng-mouseup="ngMouseup()"

View File

@@ -20,13 +20,14 @@
at runtime from the About dialog for additional information.
-->
<!-- DO NOT ADD SPACES BETWEEN THE SPANS - IT ADDS WHITE SPACE!! -->
<div class='l-indicator s-indicator s-indicator-status-{{ngModel.getGlyphClass()}}'
<div class='status block'
title="{{ngModel.getDescription()}}"
ng-click='ngModel.configure()'
ng-show="ngModel.getText().length > 0"
ng-class="ngModel.getCssClass()">
<span class="label" ng-class='ngModel.getTextClass()'>
ng-show="ngModel.getText().length > 0">
<span class="status-indicator {{ngModel.getCssClass()}}"></span><span class="label"
ng-class='ngModel.getTextClass()'>
{{ngModel.getText()}}
<a class="s-button icon-gear" ng-if="ngModel.configure"></a>
</span><span class="count">
</span>
</div>

View File

@@ -40,7 +40,7 @@ define(
// Gets an array of the contextual parents/ancestors of the selected object
function getContextualPath() {
var currentObj = $scope.domainObject,
var currentObj = $scope.ngModel.selectedObject,
currentParent,
parents = [];
@@ -68,7 +68,7 @@ define(
// If this the the initial call of this recursive function
if (!current) {
current = $scope.domainObject;
current = $scope.ngModel.selectedObject;
$scope.primaryParents = [];
}
@@ -87,16 +87,16 @@ define(
// Gets the metadata for the selected object
function getMetadata() {
$scope.metadata = $scope.domainObject &&
$scope.domainObject.hasCapability('metadata') &&
$scope.domainObject.useCapability('metadata');
$scope.metadata = $scope.ngModel.selectedObject &&
$scope.ngModel.selectedObject.hasCapability('metadata') &&
$scope.ngModel.selectedObject.useCapability('metadata');
}
// Set scope variables when the selected object changes
$scope.$watch('domainObject', function () {
$scope.isLink = $scope.domainObject &&
$scope.domainObject.hasCapability('location') &&
$scope.domainObject.getCapability('location').isLink();
$scope.$watch('ngModel.selectedObject', function () {
$scope.isLink = $scope.ngModel.selectedObject &&
$scope.ngModel.selectedObject.hasCapability('location') &&
$scope.ngModel.selectedObject.getCapability('location').isLink();
if ($scope.isLink) {
getPrimaryPath();
@@ -109,7 +109,7 @@ define(
getMetadata();
});
var mutation = $scope.domainObject.getCapability('mutation');
var mutation = $scope.ngModel.selectedObject.getCapability('mutation');
var unlisten = mutation.listen(getMetadata);
$scope.$on('$destroy', unlisten);
}

View File

@@ -1,60 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
/**
* The mct-selectable directive allows selection functionality
* (click) to be attached to specific elements.
*
* @memberof platform/commonUI/general
* @constructor
*/
function MCTSelectable(openmct) {
// Link; install event handlers.
function link(scope, element, attrs) {
var removeSelectable = openmct.selection.selectable(
element[0],
scope.$eval(attrs.mctSelectable),
attrs.hasOwnProperty('mctInitSelect') && scope.$eval(attrs.mctInitSelect) !== false
);
scope.$on("$destroy", function () {
removeSelectable();
});
}
return {
// mct-selectable only makes sense as an attribute
restrict: "A",
// Link function, to install event handlers
link: link
};
}
return MCTSelectable;
}
);

View File

@@ -41,6 +41,16 @@ define(
"$scope",
["$watch", "$on"]
);
mockScope.ngModel = {};
mockScope.ngModel.selectedObject = {
getCapability: function () {
return {
listen: function () {
return true;
}
};
}
};
mockObjectService = jasmine.createSpyObj(
"objectService",
@@ -67,27 +77,22 @@ define(
"location capability",
["isLink"]
);
mockDomainObject.getCapability.andCallFake(function (param) {
if (param === 'location') {
return mockLocationCapability;
} else if (param === 'context') {
return mockContextCapability;
} else if (param === 'mutation') {
return {
listen: function () {
return true;
}
};
}
});
mockScope.domainObject = mockDomainObject;
controller = new ObjectInspectorController(mockScope, mockObjectService);
// Change the selected object to trigger the watch call
mockScope.ngModel.selectedObject = mockDomainObject;
});
it("watches for changes to the selected object", function () {
expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function));
expect(mockScope.$watch).toHaveBeenCalledWith('ngModel.selectedObject', jasmine.any(Function));
});
it("looks for contextual parent objects", function () {

View File

@@ -65,10 +65,6 @@ define(
options = Object.create(OPTIONS);
options.marginX = -bubbleSpaceLR;
// prevent bubble from appearing right under pointer,
// which causes hover callback to be called multiple times
options.offsetX = 1;
// On a phone, bubble takes up more screen real estate,
// so position it differently (toward the bottom)
if (this.agentService.isPhone()) {

View File

@@ -1,7 +1,9 @@
<!-- DO NOT ADD SPACES BETWEEN THE SPANS - IT ADDS WHITE SPACE!! -->
<a ng-click="showNotificationsList()" ng-show="notifications.length > 0" class="l-indicator s-indicator s-indicator-status-{{highest.severity}} icon-bell"
<a ng-click="showNotificationsList()" ng-show="notifications.length > 0" class="status block"
ng-class="highest.severity"
ng-controller="NotificationIndicatorController">
<span class="label">
{{notifications.length}} Notification<span ng-show="notifications.length > 1">s</span>
<span class="status-indicator icon-bell"></span><span class="label">
{{notifications.length}} Notifications
</span><span class="count">{{notifications.length}}</span>
</a>

View File

@@ -38,8 +38,7 @@ define([
"implementation": InspectorController,
"depends": [
"$scope",
"openmct",
"$document"
"policyService"
]
}
],

View File

@@ -21,73 +21,44 @@
*****************************************************************************/
define(
[],
function () {
['../../browse/src/InspectorRegion'],
function (InspectorRegion) {
/**
* The InspectorController listens for the selection changes and adds the selection
* object to the scope.
* The InspectorController adds region data for a domain object's type
* to the scope.
*
* @constructor
*/
function InspectorController($scope, openmct, $document) {
var self = this;
self.$scope = $scope;
function InspectorController($scope, policyService) {
var domainObject = $scope.domainObject,
typeCapability = domainObject.getCapability('type'),
statusListener;
/**
* Callback handler for the selection change event.
* Adds the selection object to the scope. If the selected item has an inspector view,
* it puts the key in the scope. If provider view exists, it shows the view.
* Filters region parts to only those allowed by region policies
* @param regions
* @returns {{}}
*/
function setSelection(selection) {
if (selection[0]) {
var view = openmct.inspectorViews.get(selection);
var container = $document[0].querySelectorAll('.inspector-provider-view')[0];
container.innerHTML = "";
if (view) {
self.providerView = true;
view.show(container);
} else {
self.providerView = false;
var selectedItem = selection[0].context.oldItem;
if (selectedItem) {
$scope.inspectorKey = selectedItem.getCapability("type").typeDef.inspector;
}
}
}
self.$scope.selection = selection;
function filterRegions(inspector) {
//Dupe so we're not modifying the type definition.
return inspector.regions && inspector.regions.filter(function (region) {
return policyService.allow('region', region, domainObject);
});
}
openmct.selection.on("change", setSelection);
setSelection(openmct.selection.get());
function setRegions() {
$scope.regions = filterRegions(typeCapability.getDefinition().inspector || new InspectorRegion());
}
statusListener = domainObject.getCapability("status").listen(setRegions);
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
statusListener();
});
setRegions();
}
/**
* Gets the selected item.
*
* @returns a domain object
*/
InspectorController.prototype.selectedItem = function () {
return this.$scope.selection[0].context.oldItem;
};
/**
* Checks if a provider view exists.
*
* @returns 'true' if provider view exists, 'false' otherwise
*/
InspectorController.prototype.hasProviderView = function () {
return this.providerView;
};
return InspectorController;
}
);

View File

@@ -27,93 +27,82 @@ define(
describe("The inspector controller ", function () {
var mockScope,
mockDomainObject,
mockOpenMCT,
mockSelection,
mockInspectorViews,
mockTypeDef,
controller,
container,
$document = [],
selectable = [];
mockTypeCapability,
mockTypeDefinition,
mockPolicyService,
mockStatusCapability,
capabilities = {},
controller;
beforeEach(function () {
mockTypeDef = {
typeDef: {
inspector: "some-key"
}
mockTypeDefinition = {
inspector:
{
'regions': [
{'name': 'Part One'},
{'name': 'Part Two'}
]
}
};
mockTypeCapability = jasmine.createSpyObj('typeCapability', [
'getDefinition'
]);
mockTypeCapability.getDefinition.andReturn(mockTypeDefinition);
capabilities.type = mockTypeCapability;
mockStatusCapability = jasmine.createSpyObj('statusCapability', [
'listen'
]);
capabilities.status = mockStatusCapability;
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getCapability'
]);
mockDomainObject.getCapability.andReturn(mockTypeDef);
mockDomainObject.getCapability.andCallFake(function (name) {
return capabilities[name];
});
mockPolicyService = jasmine.createSpyObj('policyService', [
'allow'
]);
mockScope = jasmine.createSpyObj('$scope',
['$on', 'selection']
['$on']
);
selectable[0] = {
context: {
oldItem: mockDomainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn(selectable);
mockInspectorViews = jasmine.createSpyObj('inspectorViews', ['get']);
mockOpenMCT = {
selection: mockSelection,
inspectorViews: mockInspectorViews
};
container = jasmine.createSpy('container', ['innerHTML']);
$document[0] = jasmine.createSpyObj("$document", ['querySelectorAll']);
$document[0].querySelectorAll.andReturn([container]);
controller = new InspectorController(mockScope, mockOpenMCT, $document);
mockScope.domainObject = mockDomainObject;
});
it("listens for selection change event", function () {
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
expect(controller.selectedItem()).toEqual(mockDomainObject);
var mockItem = jasmine.createSpyObj('domainObject', [
'getCapability'
]);
mockItem.getCapability.andReturn(mockTypeDef);
selectable[0].context.oldItem = mockItem;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.selectedItem()).toEqual(mockItem);
it("filters out regions disallowed by region policy", function () {
mockPolicyService.allow.andReturn(false);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.regions.length).toBe(0);
});
it("cleans up on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.calls[0].args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
it("does not filter out regions allowed by region policy", function () {
mockPolicyService.allow.andReturn(true);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.regions.length).toBe(2);
});
it("adds selection object to scope", function () {
expect(mockScope.selection).toEqual(selectable);
expect(controller.selectedItem()).toEqual(mockDomainObject);
it("Responds to status changes", function () {
mockPolicyService.allow.andReturn(true);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.regions.length).toBe(2);
expect(mockStatusCapability.listen).toHaveBeenCalled();
mockPolicyService.allow.andReturn(false);
mockStatusCapability.listen.mostRecentCall.args[0]();
expect(mockScope.regions.length).toBe(0);
});
it("Unregisters status listener", function () {
var mockListener = jasmine.createSpy('listener');
mockStatusCapability.listen.andReturn(mockListener);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.$on).toHaveBeenCalledWith("$destroy", jasmine.any(Function));
mockScope.$on.mostRecentCall.args[1]();
expect(mockListener).toHaveBeenCalled();
});
});
}

View File

@@ -55,7 +55,7 @@ $colorTransLucBg: #666; // Used as a visual blocking element over variable backg
// Foundation Colors
$colorAlt1: #ffc700;
$colorAlert: #ff6600;
$colorAlert: #ff3c00;
$colorWarningHi: #cc0000;
$colorWarningLo: #ff9900;
$colorDiagnostic: #a4b442;
@@ -114,10 +114,10 @@ $colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);
// Status colors, mainly used for messaging and item ancillary symbols
$colorStatusFg: #ccc;
$colorStatusDefault: #999;
$colorStatusDefault: #ccc;
$colorStatusInfo: $colorInfo;
$colorStatusAlert: $colorAlert;
$colorStatusError: #ff3300;
$colorStatusError: #d4585c;
$colorStatusAvailable: $colorKey;
$colorStatusBtnBg: $colorBtnBg;
$colorProgressBarOuter: rgba(#000, 0.1);

View File

@@ -0,0 +1,54 @@
define([
'text!./res/templates/autoflow-tabular.html',
'./src/AutoflowTabularController',
'./src/MCTAutoflowTable'
], function (
autoflowTabularTemplate,
AutoflowTabularController,
MCTAutoflowTable
) {
return function (options) {
return function (openmct) {
openmct.legacyRegistry.register("platform/features/autoflow", {
"name": "WARP Telemetry Adapter",
"description": "Retrieves telemetry from the WARP Server and provides related types and views.",
"resources": "res",
"extensions": {
"views": [
{
"key": "autoflow",
"name": "Autoflow Tabular",
"cssClass": "icon-packet",
"description": "A tabular view of packet contents.",
"template": autoflowTabularTemplate,
"type": options && options.type,
"needs": [
"telemetry"
],
"delegation": true
}
],
"controllers": [
{
"key": "AutoflowTabularController",
"implementation": AutoflowTabularController,
"depends": [
"$scope",
"$timeout",
"telemetrySubscriber"
]
}
],
"directives": [
{
"key": "mctAutoflowTable",
"implementation": MCTAutoflowTable
}
]
}
});
openmct.legacyRegistry.enable("platform/features/autoflow");
};
};
});

View File

@@ -0,0 +1,26 @@
<div class="items-holder abs contents autoflow obj-value-format"
ng-controller="AutoflowTabularController as autoflow">
<div class="abs l-flex-row holder t-autoflow-header l-autoflow-header">
<mct-include key="'input-filter'"
ng-model="autoflow.filter"
class="flex-elem">
</mct-include>
<div class="flex-elem grows t-last-update" title="Last Update">{{autoflow.updated()}}</div>
<a title="Change column width"
class="s-button flex-elem icon-arrows-right-left change-column-width"
ng-click="autoflow.increaseColumnWidth()"></a>
</div>
<div class="abs t-autoflow-items l-autoflow-items"
mct-resize="autoflow.setBounds(bounds)"
mct-resize-interval="50">
<mct-autoflow-table values="autoflow.rangeValues()"
objects="autoflow.getTelemetryObjects()"
rows="autoflow.getRows()"
classes="autoflow.classes()"
updated="autoflow.updated()"
column-width="autoflow.columnWidth()"
counter="autoflow.counter()"
>
</mct-autoflow-table>
</div>
</div>

View File

@@ -0,0 +1,169 @@
/*global angular*/
define(
[],
function () {
/**
* The link step for the `mct-autoflow-table` directive;
* watches scope and updates the DOM appropriately.
* See documentation in `MCTAutoflowTable.js` for the rationale
* for including this directive, as well as for an explanation
* of which values are placed in scope.
*
* @constructor
* @param {Scope} scope the scope for this usage of the directive
* @param element the jqLite-wrapped element which used this directive
*/
function AutoflowTableLinker(scope, element) {
var objects, // Domain objects at last structure refresh
rows, // Number of rows from last structure refresh
priorClasses = {},
valueSpans = {}; // Span elements to put data values in
// Create a new name-value pair in the specified column
function createListItem(domainObject, ul) {
// Create a new li, and spans to go in it.
var li = angular.element('<li>'),
titleSpan = angular.element('<span>'),
valueSpan = angular.element('<span>');
// Place spans in the li, and li into the column.
// valueSpan must precede titleSpan in the DOM due to new CSS float approach
li.append(valueSpan).append(titleSpan);
ul.append(li);
// Style appropriately
li.addClass('l-autoflow-row');
titleSpan.addClass('l-autoflow-item l');
valueSpan.addClass('l-autoflow-item r l-obj-val-format');
// Set text/tooltip for the name-value row
titleSpan.text(domainObject.getModel().name);
titleSpan.attr("title", domainObject.getModel().name);
// Keep a reference to the span which will hold the
// data value, to populate in the next refreshValues call
valueSpans[domainObject.getId()] = valueSpan;
return li;
}
// Create a new column of name-value pairs in this table.
function createColumn(el) {
// Create a ul
var ul = angular.element('<ul>');
// Add it into the mct-autoflow-table
el.append(ul);
// Style appropriately
ul.addClass('l-autoflow-col');
// Get the current col width and apply at time of column creation
// Important to do this here, as new columns could be created after
// the user has changed the width.
ul.css('width', scope.columnWidth + 'px');
// Return it, so some li elements can be added
return ul;
}
// Change the width of the columns when user clicks the resize button.
function resizeColumn() {
element.find('ul').css('width', scope.columnWidth + 'px');
}
// Rebuild the DOM associated with this table.
function rebuild(domainObjects, rowCount) {
var activeColumn;
// Empty out our cached span elements
valueSpans = {};
// Start with an empty DOM beneath this directive
element.html("");
// Add DOM elements for each domain object being displayed
// in this table.
domainObjects.forEach(function (object, index) {
// Start a new column if we'd run out of room
if (index % rowCount === 0) {
activeColumn = createColumn(element);
}
// Add the DOM elements for that object to whichever
// column (a `ul` element) is current.
createListItem(object, activeColumn);
});
}
// Update spans with values, as made available via the
// `values` attribute of this directive.
function refreshValues() {
// Get the available values
var values = scope.values || {},
classes = scope.classes || {};
// Populate all spans with those values (or clear
// those spans if no value is available)
(objects || []).forEach(function (object) {
var id = object.getId(),
span = valueSpans[id],
value;
if (span) {
// Look up the value...
value = values[id];
// ...and convert to empty string if it's undefined
value = value === undefined ? "" : value;
span.attr("data-value", value);
// Update the span
span.text(value);
span.attr("title", value);
span.removeClass(priorClasses[id]);
span.addClass(classes[id]);
priorClasses[id] = classes[id];
}
// Also need stale/alert/ok class
// on span
});
}
// Refresh the DOM for this table, if necessary
function refreshStructure() {
// Only rebuild if number of rows or set of objects
// has changed; otherwise, our structure is still valid.
if (scope.objects !== objects ||
scope.rows !== rows) {
// Track those values to support future refresh checks
objects = scope.objects;
rows = scope.rows;
// Rebuild the DOM
rebuild(objects || [], rows || 1);
// Refresh all data values shown
refreshValues();
}
}
// Changing the domain objects in use or the number
// of rows should trigger a structure change (DOM rebuild)
scope.$watch("objects", refreshStructure);
scope.$watch("rows", refreshStructure);
// When the current column width has been changed, resize the column
scope.$watch('columnWidth', resizeColumn);
// When the last-updated time ticks,
scope.$watch("updated", refreshValues);
// Update displayed values when the counter changes.
scope.$watch("counter", refreshValues);
}
return AutoflowTableLinker;
}
);

View File

@@ -0,0 +1,324 @@
define(
['moment'],
function (moment) {
var ROW_HEIGHT = 16,
SLIDER_HEIGHT = 10,
INITIAL_COLUMN_WIDTH = 225,
MAX_COLUMN_WIDTH = 525,
COLUMN_WIDTH_STEP = 25,
DEBOUNCE_INTERVAL = 100,
DATE_FORMAT = "YYYY-DDD HH:mm:ss.SSS\\Z",
NOT_UPDATED = "No updates",
EMPTY_ARRAY = [];
/**
* Responsible for supporting the autoflow tabular view.
* Implements the all-over logic which drives that view,
* mediating between template-provided areas, the included
* `mct-autoflow-table` directive, and the underlying
* domain object model.
* @constructor
*/
function AutflowTabularController(
$scope,
$timeout,
telemetrySubscriber
) {
var filterValue = "",
filterValueLowercase = "",
subscription,
filteredObjects = [],
lastUpdated = {},
updateText = NOT_UPDATED,
rangeValues = {},
classes = {},
limits = {},
updatePending = false,
lastBounce = Number.NEGATIVE_INFINITY,
columnWidth = INITIAL_COLUMN_WIDTH,
rows = 1,
counter = 0;
// Trigger an update of the displayed table by incrementing
// the counter that it watches.
function triggerDisplayUpdate() {
counter += 1;
}
// Check whether or not an object's name matches the
// user-entered filter value.
function filterObject(domainObject) {
return (domainObject.getModel().name || "")
.toLowerCase()
.indexOf(filterValueLowercase) !== -1;
}
// Comparator for sorting points back into packet order
function compareObject(objectA, objectB) {
var indexA = objectA.getModel().index || 0,
indexB = objectB.getModel().index || 0;
return indexA - indexB;
}
// Update the list of currently-displayed objects; these
// will be the subset of currently subscribed-to objects
// which match a user-entered filter.
function doUpdateFilteredObjects() {
// Generate the list
filteredObjects = (
subscription ?
subscription.getTelemetryObjects() :
[]
).filter(filterObject).sort(compareObject);
// Clear the pending flag
updatePending = false;
// Track when this occurred, so that we can wait
// a whole before updating again.
lastBounce = Date.now();
triggerDisplayUpdate();
}
// Request an update to the list of current objects; this may
// run on a timeout to avoid excessive calls, e.g. while the user
// is typing a filter.
function updateFilteredObjects() {
// Don't do anything if an update is already scheduled
if (!updatePending) {
if (Date.now() > lastBounce + DEBOUNCE_INTERVAL) {
// Update immediately if it's been long enough
doUpdateFilteredObjects();
} else {
// Otherwise, update later, and track that we have
// an update pending so that subsequent calls can
// be ignored.
updatePending = true;
$timeout(doUpdateFilteredObjects, DEBOUNCE_INTERVAL);
}
}
}
// Track the latest data values for this domain object
function recordData(telemetryObject) {
// Get latest domain/range values for this object.
var id = telemetryObject.getId(),
domainValue = subscription.getDomainValue(telemetryObject),
rangeValue = subscription.getRangeValue(telemetryObject);
// Track the most recent timestamp change observed...
if (domainValue !== undefined && domainValue !== lastUpdated[id]) {
lastUpdated[id] = domainValue;
// ... and update the displayable text for that timestamp
updateText = isNaN(domainValue) ? "" :
moment.utc(domainValue).format(DATE_FORMAT);
}
// Store data values into the rangeValues structure, which
// will be used to populate the table itself.
// Note that we want full precision here.
rangeValues[id] = rangeValue;
// Update limit states as well
classes[id] = limits[id] && (limits[id].evaluate({
// This relies on external knowledge that the
// range value of a telemetry point is encoded
// in its datum as "value."
value: rangeValue
}) || {}).cssClass;
}
// Look at telemetry objects from the subscription; this is watched
// to detect changes from the subscription.
function subscribedTelemetry() {
return subscription ?
subscription.getTelemetryObjects() : EMPTY_ARRAY;
}
// Update the data values which will be used to populate the table
function updateValues() {
subscribedTelemetry().forEach(recordData);
triggerDisplayUpdate();
}
// Getter-setter function for user-entered filter text.
function filter(value) {
// If value was specified, we're a setter
if (value !== undefined) {
// Store the new value
filterValue = value;
filterValueLowercase = value.toLowerCase();
// Change which objects appear in the table
updateFilteredObjects();
}
// Always act as a getter
return filterValue;
}
// Update the bounds (width and height) of this view;
// called from the mct-resize directive. Recalculates how
// many rows should appear in the contained table.
function setBounds(bounds) {
var availableSpace = bounds.height - SLIDER_HEIGHT;
rows = Math.max(1, Math.floor(availableSpace / ROW_HEIGHT));
}
// Increment the current column width, up to the defined maximum.
// When the max is hit, roll back to the default.
function increaseColumnWidth() {
columnWidth += COLUMN_WIDTH_STEP;
// Cycle down to the initial width instead of exceeding max
columnWidth = columnWidth > MAX_COLUMN_WIDTH ?
INITIAL_COLUMN_WIDTH : columnWidth;
}
// Get displayable text for last-updated value
function updated() {
return updateText;
}
// Unsubscribe, if a subscription is active.
function releaseSubscription() {
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
}
}
// Update set of telemetry objects managed by this view
function updateTelemetryObjects(telemetryObjects) {
updateFilteredObjects();
limits = {};
telemetryObjects.forEach(function (telemetryObject) {
var id = telemetryObject.getId();
limits[id] = telemetryObject.getCapability('limit');
});
}
// Create a subscription for the represented domain object.
// This will resolve capability delegation as necessary.
function makeSubscription(domainObject) {
// Unsubscribe, if there is an existing subscription
releaseSubscription();
// Clear updated timestamp
lastUpdated = {};
updateText = NOT_UPDATED;
// Create a new subscription; telemetrySubscriber gets
// to do the meaningful work here.
subscription = domainObject && telemetrySubscriber.subscribe(
domainObject,
updateValues
);
// Our set of in-view telemetry objects may have changed,
// so update the set that is being passed down to the table.
updateFilteredObjects();
}
// Watch for changes to the set of objects which have telemetry
$scope.$watch(subscribedTelemetry, updateTelemetryObjects);
// Watch for the represented domainObject (this field will
// be populated by mct-representation)
$scope.$watch("domainObject", makeSubscription);
// Make sure we unsubscribe when this view is destroyed.
$scope.$on("$destroy", releaseSubscription);
return {
/**
* Get the number of rows which should be shown in this table.
* @return {number} the number of rows to show
*/
getRows: function () {
return rows;
},
/**
* Get the objects which should currently be displayed in
* this table. This will be watched, so the return value
* should be stable when this list is unchanging. Only
* objects which match the user-entered filter value should
* be returned here.
* @return {DomainObject[]} the domain objects to include in
* this table.
*/
getTelemetryObjects: function () {
return filteredObjects;
},
/**
* Set the bounds (width/height) of this autoflow tabular view.
* The template must ensure that these bounds are tracked on
* the table area only.
* @param bounds the bounds; and object with `width` and
* `height` properties, both as numbers, in pixels.
*/
setBounds: setBounds,
/**
* Increments the width of the autoflow column.
* Setting does not yet persist.
*/
increaseColumnWidth: increaseColumnWidth,
/**
* Get-or-set the user-supplied filter value.
* @param {string} [value] the new filter value; omit to use
* as a getter
* @returns {string} the user-supplied filter value
*/
filter: filter,
/**
* Get all range values for use in this table. These will be
* returned as an object of key-value pairs, where keys are
* domain object IDs, and values are the most recently observed
* data values associated with those objects, formatted for
* display.
* @returns {object.<string,string>} most recent values
*/
rangeValues: function () {
return rangeValues;
},
/**
* Get CSS classes to apply to specific rows, representing limit
* states and/or stale states. These are returned as key-value
* pairs where keys are domain object IDs, and values are CSS
* classes to display for domain objects with those IDs.
* @returns {object.<string,string>} CSS classes
*/
classes: function () {
return classes;
},
/**
* Get the "last updated" text for this view; this will be
* the most recent timestamp observed for any telemetry-
* providing object, formatted for display.
* @returns {string} the time of the most recent update
*/
updated: updated,
/**
* Get the current column width, in pixels.
* @returns {number} column width
*/
columnWidth: function () {
return columnWidth;
},
/**
* Keep a counter and increment this whenever the display
* should be updated; this will be watched by the
* `mct-autoflow-table`.
* @returns {number} a counter value
*/
counter: function () {
return counter;
}
};
}
return AutflowTabularController;
}
);

View File

@@ -0,0 +1,60 @@
define(
["./AutoflowTableLinker"],
function (AutoflowTableLinker) {
/**
* The `mct-autoflow-table` directive specifically supports
* autoflow tabular views; it is not intended for use outside
* of that view.
*
* This directive is responsible for creating the structure
* of the table in this view, and for updating its values.
* While this is achievable using a regular Angular template,
* this is undesirable from the perspective of performance
* due to the number of watches that can be involved for large
* tables. Instead, this directive will maintain a small number
* of watches, rebuilding table structure only when necessary,
* and updating displayed values in the more common case of
* new data arriving.
*
* @constructor
*/
function MCTAutoflowTable() {
return {
// Only applicable at the element level
restrict: "E",
// The link function; handles DOM update/manipulation
link: AutoflowTableLinker,
// Parameters to pass from attributes into scope
scope: {
// Set of domain objects to show in the table
objects: "=",
// Values for those objects, by ID
values: "=",
// CSS classes to show for objects, by ID
classes: "=",
// Number of rows to show before autoflowing
rows: "=",
// Time of last update; watched to refresh values
updated: "=",
// Current width of the autoflow column
columnWidth: "=",
// A counter used to trigger display updates
counter: "="
}
};
}
return MCTAutoflowTable;
}
);

View File

@@ -0,0 +1,178 @@
define(
["../src/AutoflowTableLinker"],
function (AutoflowTableLinker) {
describe("The mct-autoflow-table linker", function () {
var cachedAngular,
mockAngular,
mockScope,
mockElement,
mockElements,
linker;
// Utility function to generate more mock elements
function createMockElement(html) {
var mockEl = jasmine.createSpyObj(
"element-" + html,
[
"append",
"addClass",
"removeClass",
"text",
"attr",
"html",
"css",
"find"
]
);
mockEl.testHtml = html;
mockEl.append.andReturn(mockEl);
mockElements.push(mockEl);
return mockEl;
}
function createMockDomainObject(id) {
var mockDomainObject = jasmine.createSpyObj(
"domainObject-" + id,
["getId", "getModel"]
);
mockDomainObject.getId.andReturn(id);
mockDomainObject.getModel.andReturn({name: id.toUpperCase()});
return mockDomainObject;
}
function fireWatch(watchExpression, value) {
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === watchExpression) {
call.args[1](value);
}
});
}
// AutoflowTableLinker accesses Angular in the global
// scope, since it is not injectable; we simulate that
// here by adding/removing it to/from the window object.
beforeEach(function () {
mockElements = [];
mockAngular = jasmine.createSpyObj("angular", ["element"]);
mockScope = jasmine.createSpyObj("scope", ["$watch"]);
mockElement = createMockElement('<div>');
mockAngular.element.andCallFake(createMockElement);
if (window.angular !== undefined) {
cachedAngular = window.angular;
}
window.angular = mockAngular;
linker = new AutoflowTableLinker(mockScope, mockElement);
});
afterEach(function () {
if (cachedAngular !== undefined) {
window.angular = cachedAngular;
} else {
delete window.angular;
}
});
it("watches for changes in inputs", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"objects",
jasmine.any(Function)
);
expect(mockScope.$watch).toHaveBeenCalledWith(
"rows",
jasmine.any(Function)
);
expect(mockScope.$watch).toHaveBeenCalledWith(
"counter",
jasmine.any(Function)
);
});
it("changes structure when domain objects change", function () {
// Set up scope
mockScope.rows = 4;
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
.map(createMockDomainObject);
// Fire an update to the set of objects
fireWatch("objects");
// Should have rebuilt with two columns of
// four and two rows each; first, by clearing...
expect(mockElement.html).toHaveBeenCalledWith("");
// Should have appended two columns...
expect(mockElement.append.calls.length).toEqual(2);
// ...which should have received two and four rows each
expect(mockElement.append.calls[0].args[0].append.calls.length)
.toEqual(4);
expect(mockElement.append.calls[1].args[0].append.calls.length)
.toEqual(2);
});
it("updates values", function () {
var mockSpans;
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
.map(createMockDomainObject);
mockScope.values = { a: 0 };
// Fire an update to the set of values
fireWatch("objects");
fireWatch("updated");
// Get all created spans
mockSpans = mockElements.filter(function (mockElem) {
return mockElem.testHtml === '<span>';
});
// First span should be a, should have gotten this value.
// This test detects, in particular, WTD-749
expect(mockSpans[0].text).toHaveBeenCalledWith('A');
expect(mockSpans[1].text).toHaveBeenCalledWith(0);
});
it("listens for changes in column width", function () {
var mockUL = createMockElement("<ul>");
mockElement.find.andReturn(mockUL);
mockScope.columnWidth = 200;
fireWatch("columnWidth", mockScope.columnWidth);
expect(mockUL.css).toHaveBeenCalledWith("width", "200px");
});
it("updates CSS classes", function () {
var mockSpans;
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
.map(createMockDomainObject);
mockScope.values = { a: "a value to find" };
mockScope.classes = { a: 'class-a' };
// Fire an update to the set of values
fireWatch("objects");
fireWatch("updated");
// Figure out which span holds the relevant value...
mockSpans = mockElements.filter(function (mockElem) {
return mockElem.testHtml === '<span>';
}).filter(function (mockSpan) {
var attrCalls = mockSpan.attr.calls;
return attrCalls.some(function (call) {
return call.args[0] === 'title' &&
call.args[1] === mockScope.values.a;
});
});
// ...and make sure it also has had its class applied
expect(mockSpans[0].addClass)
.toHaveBeenCalledWith(mockScope.classes.a);
});
});
}
);

View File

@@ -0,0 +1,341 @@
define(
["../src/AutoflowTabularController"],
function (AutoflowTabularController) {
describe("The autoflow tabular controller", function () {
var mockScope,
mockTimeout,
mockSubscriber,
mockDomainObject,
mockSubscription,
controller;
// Fire watches that are registered as functions.
function fireFnWatches() {
mockScope.$watch.calls.forEach(function (call) {
if (typeof call.args[0] === 'function') {
call.args[1](call.args[0]());
}
});
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
["$on", "$watch"]
);
mockTimeout = jasmine.createSpy("$timeout");
mockSubscriber = jasmine.createSpyObj(
"telemetrySubscriber",
["subscribe"]
);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getId", "getModel", "getCapability"]
);
mockSubscription = jasmine.createSpyObj(
"subscription",
[
"unsubscribe",
"getTelemetryObjects",
"getDomainValue",
"getRangeValue"
]
);
mockSubscriber.subscribe.andReturn(mockSubscription);
mockDomainObject.getModel.andReturn({name: "something"});
controller = new AutoflowTabularController(
mockScope,
mockTimeout,
mockSubscriber
);
});
it("listens for the represented domain object", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"domainObject",
jasmine.any(Function)
);
});
it("provides a getter-setter function for filtering", function () {
expect(controller.filter()).toEqual("");
controller.filter("something");
expect(controller.filter()).toEqual("something");
});
it("tracks bounds and adjust number of rows accordingly", function () {
// Rows are 15px high, and need room for an 10px slider
controller.setBounds({ width: 700, height: 120 });
expect(controller.getRows()).toEqual(6); // 110 usable height / 16px
controller.setBounds({ width: 700, height: 240 });
expect(controller.getRows()).toEqual(14); // 230 usable height / 16px
});
it("subscribes to a represented object's telemetry", function () {
// Set up subscription, scope
mockSubscription.getTelemetryObjects
.andReturn([mockDomainObject]);
mockScope.domainObject = mockDomainObject;
// Invoke the watcher with represented domain object
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
// Should have subscribed to it
expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
mockDomainObject,
jasmine.any(Function)
);
// Should report objects as reported from subscription
expect(controller.getTelemetryObjects())
.toEqual([mockDomainObject]);
});
it("releases subscriptions on destroy", function () {
// Set up subscription...
mockSubscription.getTelemetryObjects
.andReturn([mockDomainObject]);
mockScope.domainObject = mockDomainObject;
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
// Verify precondition
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
// Make sure we're listening for $destroy
expect(mockScope.$on).toHaveBeenCalledWith(
"$destroy",
jasmine.any(Function)
);
// Fire a destroy event
mockScope.$on.mostRecentCall.args[1]();
// Should have unsubscribed
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
});
it("presents latest values and latest update state", function () {
// Make sure values are available
mockSubscription.getDomainValue.andReturn(402654321123);
mockSubscription.getRangeValue.andReturn(789);
mockDomainObject.getId.andReturn('testId');
// Set up subscription...
mockSubscription.getTelemetryObjects
.andReturn([mockDomainObject]);
mockScope.domainObject = mockDomainObject;
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
// Fire subscription callback
mockSubscriber.subscribe.mostRecentCall.args[1]();
// ...and exposed the results for template to consume
expect(controller.updated()).toEqual("1982-278 08:25:21.123Z");
expect(controller.rangeValues().testId).toEqual(789);
});
it("sorts domain objects by index", function () {
var testIndexes = { a: 2, b: 1, c: 3, d: 0 },
mockDomainObjects = Object.keys(testIndexes).sort().map(function (id) {
var mockDomainObj = jasmine.createSpyObj(
"domainObject",
["getId", "getModel"]
);
mockDomainObj.getId.andReturn(id);
mockDomainObj.getModel.andReturn({ index: testIndexes[id] });
return mockDomainObj;
});
// Expose those domain objects...
mockSubscription.getTelemetryObjects.andReturn(mockDomainObjects);
mockScope.domainObject = mockDomainObject;
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
// Fire subscription callback
mockSubscriber.subscribe.mostRecentCall.args[1]();
// Controller should expose same objects, but sorted by index from model
expect(controller.getTelemetryObjects()).toEqual([
mockDomainObjects[3], // d, index=0
mockDomainObjects[1], // b, index=1
mockDomainObjects[0], // a, index=2
mockDomainObjects[2] // c, index=3
]);
});
it("uses a timeout to throttle update", function () {
// Set up subscription...
mockSubscription.getTelemetryObjects
.andReturn([mockDomainObject]);
mockScope.domainObject = mockDomainObject;
// Set the object in view; should not need a timeout
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockTimeout.calls.length).toEqual(0);
// Next call should schedule an update on a timeout
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockTimeout.calls.length).toEqual(1);
// ...but this last one should not, since existing
// timeout will cover it
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockTimeout.calls.length).toEqual(1);
});
it("allows changing column width", function () {
var initialWidth = controller.columnWidth();
controller.increaseColumnWidth();
expect(controller.columnWidth()).toBeGreaterThan(initialWidth);
});
describe("filter", function () {
var doFilter,
filteredObjects,
filteredObjectNames;
beforeEach(function () {
var telemetryObjects,
updateFilteredObjects;
telemetryObjects = [
'DEF123',
'abc789',
'456abc',
'4ab3cdef',
'hjs[12].*(){}^\\'
].map(function (objectName, index) {
var mockTelemetryObject = jasmine.createSpyObj(
objectName,
["getId", "getModel"]
);
mockTelemetryObject.getId.andReturn(objectName);
mockTelemetryObject.getModel.andReturn({
name: objectName,
index: index
});
return mockTelemetryObject;
});
mockSubscription
.getTelemetryObjects
.andReturn(telemetryObjects);
// Trigger domainObject change to create subscription.
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
updateFilteredObjects = function () {
filteredObjects = controller.getTelemetryObjects();
filteredObjectNames = filteredObjects.map(function (o) {
return o.getModel().name;
});
};
doFilter = function (term) {
controller.filter(term);
// Filter is debounced so we have to force it to occur.
mockTimeout.mostRecentCall.args[0]();
updateFilteredObjects();
};
updateFilteredObjects();
});
it("initially shows all objects", function () {
expect(filteredObjectNames).toEqual([
'DEF123',
'abc789',
'456abc',
'4ab3cdef',
'hjs[12].*(){}^\\'
]);
});
it("by blank string matches all objects", function () {
doFilter('');
expect(filteredObjectNames).toEqual([
'DEF123',
'abc789',
'456abc',
'4ab3cdef',
'hjs[12].*(){}^\\'
]);
});
it("exactly matches an object name", function () {
doFilter('4ab3cdef');
expect(filteredObjectNames).toEqual(['4ab3cdef']);
});
it("partially matches object names", function () {
doFilter('abc');
expect(filteredObjectNames).toEqual([
'abc789',
'456abc'
]);
});
it("matches case insensitive names", function () {
doFilter('def');
expect(filteredObjectNames).toEqual([
'DEF123',
'4ab3cdef'
]);
});
it("works as expected with special characters", function () {
doFilter('[12]');
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
doFilter('.*');
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
doFilter('.*()');
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
doFilter('.*?');
expect(filteredObjectNames).toEqual([]);
doFilter('.+');
expect(filteredObjectNames).toEqual([]);
});
it("exposes CSS classes from limits", function () {
var id = mockDomainObject.getId(),
testClass = "some-css-class",
mockLimitCapability =
jasmine.createSpyObj('limit', ['evaluate']);
mockDomainObject.getCapability.andCallFake(function (key) {
return key === 'limit' && mockLimitCapability;
});
mockLimitCapability.evaluate
.andReturn({ cssClass: testClass });
mockSubscription.getTelemetryObjects
.andReturn([mockDomainObject]);
fireFnWatches();
mockSubscriber.subscribe.mostRecentCall.args[1]();
expect(controller.classes()[id]).toEqual(testClass);
});
it("exposes a counter that changes with each update", function () {
var i, prior;
for (i = 0; i < 10; i += 1) {
prior = controller.counter();
expect(controller.counter()).toEqual(prior);
mockSubscriber.subscribe.mostRecentCall.args[1]();
expect(controller.counter()).not.toEqual(prior);
}
});
});
});
}
);

View File

@@ -0,0 +1,39 @@
define(
["../src/MCTAutoflowTable"],
function (MCTAutoflowTable) {
describe("The mct-autoflow-table directive", function () {
var mctAutoflowTable;
beforeEach(function () {
mctAutoflowTable = new MCTAutoflowTable();
});
// Real functionality is contained/tested in the linker,
// so just check to make sure we're exposing the directive
// appropriately.
it("is applicable at the element level", function () {
expect(mctAutoflowTable.restrict).toEqual("E");
});
it("two-ways binds needed scope variables", function () {
expect(mctAutoflowTable.scope).toEqual({
objects: "=",
values: "=",
rows: "=",
updated: "=",
classes: "=",
columnWidth: "=",
counter: "="
});
});
it("provides a link function", function () {
expect(mctAutoflowTable.link).toEqual(jasmine.any(Function));
});
});
}
);

View File

@@ -45,11 +45,11 @@ define(
}
ClockIndicator.prototype.getGlyphClass = function () {
return "subdued";
return "no-collapse float-right subdued";
};
ClockIndicator.prototype.getCssClass = function () {
return "icon-clock no-collapse float-right";
return "icon-clock";
};
ClockIndicator.prototype.getText = function () {

View File

@@ -36,16 +36,16 @@ define(
}
FollowIndicator.prototype.getGlyphClass = function () {
return (this.timerService.getTimer()) ? "ok" : "none";
return "";
};
FollowIndicator.prototype.getCssClass = function () {
return "icon-timer";
return (this.timerService.getTimer()) ? "icon-timer s-status-ok" : "icon-timer";
};
FollowIndicator.prototype.getText = function () {
var timer = this.timerService.getTimer();
return timer ? ('Following timer ' + timer.name) : NO_TIMER;
return (timer) ? 'Following timer ' + timer.getModel().name : NO_TIMER;
};
FollowIndicator.prototype.getDescription = function () {

View File

@@ -42,15 +42,18 @@ define(["../../src/indicators/FollowIndicator"], function (FollowIndicator) {
});
describe("when a timer is set", function () {
var testObject;
var testModel;
var mockDomainObject;
beforeEach(function () {
testObject = { name: "some timer!" };
mockTimerService.getTimer.andReturn(testObject);
testModel = { name: "some timer!" };
mockDomainObject = jasmine.createSpyObj('timer', ['getModel']);
mockDomainObject.getModel.andReturn(testModel);
mockTimerService.getTimer.andReturn(mockDomainObject);
});
it("displays the timer's name", function () {
expect(indicator.getText().indexOf(testObject.name))
expect(indicator.getText().indexOf(testModel.name))
.not.toEqual(-1);
});
});

View File

@@ -255,8 +255,6 @@ define(
if (this.nextDatum) {
this.updateValues(this.nextDatum);
delete this.nextDatum;
} else {
this.updateValues(this.$scope.imageHistory[this.$scope.imageHistory.length - 1]);
}
this.autoScroll = true;
}

View File

@@ -183,17 +183,6 @@ define(
expect(controller.getImageUrl()).toEqual(newUrl);
});
it("forwards large image view to latest image in history on un-pause", function () {
$scope.imageHistory = [
{ utc: 1434600258122, url: 'some/url1', selected: false},
{ utc: 1434600258123, url: 'some/url2', selected: false}
];
controller.paused(true);
controller.paused(false);
expect(controller.getImageUrl()).toEqual(controller.getImageUrl($scope.imageHistory[1]));
});
it("subscribes to telemetry", function () {
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(
newDomainObject,
@@ -238,7 +227,7 @@ define(
expect(controller.updateHistory(mockDatum)).toBe(false);
});
describe("when user clicks on imagery thumbnail", function () {
describe("user clicks on imagery thumbnail", function () {
var mockDatum = { utc: 1434600258123, url: 'some/url', selected: false};
it("pauses and adds selected class to imagery thumbnail", function () {
@@ -259,7 +248,6 @@ define(
expect(controller.getTime()).toEqual(controller.timeFormat.format(mockDatum.utc));
});
});
});
it("initially shows an empty string for date/time", function () {

View File

@@ -260,9 +260,7 @@ define([
"key": "LayoutController",
"implementation": LayoutController,
"depends": [
"$scope",
"$element",
"openmct"
"$scope"
]
},
{
@@ -272,8 +270,7 @@ define([
"$scope",
"$q",
"dialogService",
"openmct",
"$element"
"openmct"
]
}
],

View File

@@ -23,7 +23,7 @@
ng-controller="FixedController as controller">
<!-- Background grid -->
<div class="l-grid-holder" ng-click="controller.bypassSelection($event)">
<div class="l-grid-holder" ng-click="controller.clearSelection()">
<div class="l-grid l-grid-x"
ng-if="!controller.getGridSize()[0] < 3"
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
@@ -35,28 +35,33 @@
<!-- Fixed position elements -->
<div ng-repeat="element in controller.getElements()"
class="l-fixed-position-item s-selectable s-moveable s-hover-border"
ng-class="{
's-not-selected': controller.selected() && !controller.selected(element),
's-selected': controller.selected(element)
}"
ng-style="element.style"
mct-selectable="controller.getContext(element)"
mct-init-select="controller.shouldSelect(element)">
ng-click="controller.select(element)">
<mct-include key="element.template"
parameters="{ gridSize: controller.getGridSize() }"
ng-model="element">
</mct-include>
</mct-include>
</div>
<!-- Selection highlight, handles -->
<span class="s-selected s-moveable" ng-if="controller.isElementSelected()">
<span class="s-selected s-moveable" ng-if="controller.selected()">
<div class="l-fixed-position-item t-edit-handle-holder"
mct-drag-down="controller.moveHandle().startDrag()"
mct-drag-down="controller.moveHandle().startDrag(controller.selected())"
mct-drag="controller.moveHandle().continueDrag(delta)"
mct-drag-up="controller.endDrag()"
ng-style="controller.getSelectedElementStyle()">
mct-drag-up="controller.moveHandle().endDrag()"
ng-style="controller.selected().style">
</div>
<div ng-repeat="handle in controller.handles()"
class="l-fixed-position-item-handle edit-corner"
ng-style="handle.style()"
mct-drag-down="handle.startDrag()"
mct-drag="handle.continueDrag(delta)"
mct-drag-up="controller.endDrag(handle)">
mct-drag-up="handle.endDrag()">
</div>
</span>
</div>

View File

@@ -22,12 +22,10 @@
<div class="abs l-layout"
ng-controller="LayoutController as controller"
ng-click="controller.bypassSelection($event)">
ng-click="controller.clearSelection()">
<!-- Background grid -->
<div class="l-grid-holder"
ng-show="!controller.drilledIn"
ng-click="controller.bypassSelection($event)">
<div class="l-grid-holder" ng-click="controller.clearSelection()">
<div class="l-grid l-grid-x"
ng-if="!controller.getGridSize()[0] < 3"
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
@@ -36,13 +34,10 @@
ng-style="{ 'background-size': '100% ' + controller.getGridSize() [1] + 'px' }"></div>
</div>
<div class="abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border t-object-type-{{ childObject.getModel().type }}"
data-layout-id="{{childObject.getId() + '-' + $id}}"
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-drilled-in': controller.isDrilledIn(childObject) }"
<div class='abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border'
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-selected':controller.selected(childObject) }"
ng-repeat="childObject in composition"
ng-init="controller.selectIfNew(childObject.getId() + '-' + $id, childObject)"
mct-selectable="controller.getContext(childObject, true)"
ng-dblclick="controller.drill($event, childObject)"
ng-click="controller.select($event, childObject.getId())"
ng-style="controller.getFrameStyle(childObject.getId())">
<mct-representation key="'frame'"
@@ -50,7 +45,7 @@
mct-object="childObject">
</mct-representation>
<!-- Drag handles -->
<span class="abs t-edit-handle-holder" ng-if="controller.selected(childObject) && !controller.isDrilledIn(childObject)">
<span class="abs t-edit-handle-holder s-hover-border" ng-if="controller.selected(childObject)">
<span class="edit-handle edit-move"
mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [0,0])"
mct-drag="controller.continueDrag(delta)"
@@ -78,6 +73,7 @@
mct-drag-up="controller.endDrag()">
</span>
</span>
</div>
</div>

View File

@@ -47,7 +47,7 @@ define(
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function FixedController($scope, $q, dialogService, openmct, $element) {
function FixedController($scope, $q, dialogService, openmct) {
this.names = {}; // Cache names by ID
this.values = {}; // Cache values by ID
this.elementProxiesById = {};
@@ -55,11 +55,9 @@ define(
this.telemetryObjects = [];
this.subscriptions = [];
this.openmct = openmct;
this.$element = $element;
this.$scope = $scope;
this.gridSize = $scope.domainObject && $scope.domainObject.getModel().layoutGrid;
this.fixedViewSelectable = false;
var self = this;
[
@@ -89,8 +87,9 @@ define(
// Update the style for a selected element
function updateSelectionStyle() {
if (self.selectedElementProxy) {
self.selectedElementProxy.style = convertPosition(self.selectedElementProxy);
var element = self.selection && self.selection.get();
if (element) {
element.style = convertPosition(element);
}
}
@@ -137,19 +136,25 @@ define(
// Decorate elements in the current configuration
function refreshElements() {
var elements = (($scope.configuration || {}).elements || []);
// Cache selection; we are instantiating new proxies
// so we may want to restore this.
var selected = self.selection && self.selection.get(),
elements = (($scope.configuration || {}).elements || []),
index = -1; // Start with a 'not-found' value
// Find the selection in the new array
if (selected !== undefined) {
index = elements.indexOf(selected.element);
}
// Create the new proxies...
self.elementProxies = elements.map(makeProxyElement);
// If selection is not in array, select parent.
// Otherwise, set the element to select after refresh.
if (self.selectedElementProxy) {
var index = elements.indexOf(self.selectedElementProxy.element);
if (index === -1) {
self.$element[0].click();
} else if (!self.elementToSelectAfterRefresh) {
self.elementToSelectAfterRefresh = self.elementProxies[index].element;
// Clear old selection, and restore if appropriate
if (self.selection) {
self.selection.deselect();
if (index > -1) {
self.select(self.elementProxies[index]);
}
}
@@ -219,12 +224,12 @@ define(
$scope.configuration.elements || [];
// Store the position of this element.
$scope.configuration.elements.push(element);
self.elementToSelectAfterRefresh = element;
// Refresh displayed elements
refreshElements();
// Select the newly-added element
self.select(
self.elementProxies[self.elementProxies.length - 1]
);
// Mark change as persistable
if ($scope.commit) {
$scope.commit("Dropped an element.");
@@ -258,36 +263,21 @@ define(
self.getTelemetry($scope.domainObject);
}
// Sets the selectable object in response to the selection change event.
function setSelection(selectable) {
var selection = selectable[0];
if (!selection) {
return;
}
if (selection.context.elementProxy) {
self.selectedElementProxy = selection.context.elementProxy;
self.mvHandle = self.generateDragHandle(self.selectedElementProxy);
self.resizeHandles = self.generateDragHandles(self.selectedElementProxy);
} else {
// Make fixed view selectable if it's not already.
if (!self.fixedViewSelectable) {
self.fixedViewSelectable = true;
selection.context.viewProxy = new FixedProxy(addElement, $q, dialogService);
self.openmct.selection.select(selection);
}
self.resizeHandles = [];
self.mvHandle = undefined;
self.selectedElementProxy = undefined;
}
}
this.elementProxies = [];
this.generateDragHandle = generateDragHandle;
this.generateDragHandles = generateDragHandles;
this.updateSelectionStyle = updateSelectionStyle;
// Track current selection state
$scope.$watch("selection", function (selection) {
this.selection = selection;
// Expose the view's selection proxy
if (this.selection) {
this.selection.proxy(
new FixedProxy(addElement, $q, dialogService)
);
}
}.bind(this));
// Detect changes to grid size
$scope.$watch("model.layoutGrid", updateElementPositions);
@@ -308,13 +298,10 @@ define(
$scope.$on("$destroy", function () {
self.unsubscribe();
self.openmct.time.off("bounds", updateDisplayBounds);
self.openmct.selection.off("change", setSelection);
});
// Respond to external bounds changes
this.openmct.time.on("bounds", updateDisplayBounds);
this.openmct.selection.on('change', setSelection);
this.$element.on('click', this.bypassSelection.bind(this));
}
/**
@@ -373,47 +360,22 @@ define(
*/
FixedController.prototype.updateView = function (telemetryObject, datum) {
var metadata = this.openmct.telemetry.getMetadata(telemetryObject);
var telemetryKeyToDisplay = this.chooseTelemetryKeyToDisplay(metadata);
var formattedTelemetryValue = this.getFormattedTelemetryValueForKey(telemetryKeyToDisplay, datum, metadata);
var rangeMetadata = metadata.valuesForHints(['range'])[0];
var rangeKey = rangeMetadata.source || rangeMetadata.key;
var valueMetadata = metadata.value(rangeKey);
var limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
var alarm = limitEvaluator && limitEvaluator.evaluate(datum, telemetryKeyToDisplay);
var formatter = this.openmct.telemetry.getValueFormatter(valueMetadata);
var value = datum[valueMetadata.key];
var alarm = limitEvaluator && limitEvaluator.evaluate(datum, rangeKey);
this.setDisplayedValue(
telemetryObject,
formattedTelemetryValue,
formatter.format(value),
alarm && alarm.cssClass
);
this.digest();
};
/**
* @private
*/
FixedController.prototype.getFormattedTelemetryValueForKey = function (telemetryKeyToDisplay, datum, metadata) {
var valueMetadata = metadata.value(telemetryKeyToDisplay);
var formatter = this.openmct.telemetry.getValueFormatter(valueMetadata);
return formatter.format(datum[valueMetadata.key]);
};
/**
* @private
*/
FixedController.prototype.chooseTelemetryKeyToDisplay = function (metadata) {
// If there is a range value, show that preferentially
var telemetryKeyToDisplay = metadata.valuesForHints(['range'])[0];
// If no range is defined, default to the highest priority non time-domain data.
if (telemetryKeyToDisplay === undefined) {
var valuesOrderedByPriority = metadata.values();
telemetryKeyToDisplay = valuesOrderedByPriority.filter(function (valueMetadata) {
return !(valueMetadata.hints.domain);
})[0];
}
return telemetryKeyToDisplay.source;
};
/**
* Request the last historical data point for the given domain objects
* @param {object[]} objects
@@ -426,9 +388,7 @@ define(
objects.forEach(function (object) {
self.openmct.telemetry.request(object, {start: bounds.start, end: bounds.end, size: 1})
.then(function (data) {
if (data.length > 0) {
self.updateView(object, data[data.length - 1]);
}
self.updateView(object, data[data.length - 1]);
});
});
return objects;
@@ -505,56 +465,38 @@ define(
};
/**
* Checks if the element should be selected or not.
*
* @param elementProxy the element to check
* @returns {boolean} true if the element should be selected.
* Check if the element is currently selected, or (if no
* argument is supplied) get the currently selected element.
* @returns {boolean} true if selected
*/
FixedController.prototype.shouldSelect = function (elementProxy) {
if (elementProxy.element === this.elementToSelectAfterRefresh) {
delete this.elementToSelectAfterRefresh;
return true;
} else {
return false;
FixedController.prototype.selected = function (element) {
var selection = this.selection;
return selection && ((arguments.length > 0) ?
selection.selected(element) : selection.get());
};
/**
* Set the active user selection in this view.
* @param element the element to select
*/
FixedController.prototype.select = function select(element) {
if (this.selection) {
// Update selection...
this.selection.select(element);
// ...as well as move, resize handles
this.mvHandle = this.generateDragHandle(element);
this.resizeHandles = this.generateDragHandles(element);
}
};
/**
* Checks if an element is currently selected.
*
* @returns {boolean} true if an element is selected.
* Clear the current user selection.
*/
FixedController.prototype.isElementSelected = function () {
return (this.selectedElementProxy) ? true : false;
};
/**
* Gets the style for the selected element.
*
* @returns {string} element style
*/
FixedController.prototype.getSelectedElementStyle = function () {
return (this.selectedElementProxy) ? this.selectedElementProxy.style : undefined;
};
/**
* Gets the selected element.
*
* @returns the selected element
*/
FixedController.prototype.getSelectedElement = function () {
return this.selectedElementProxy;
};
/**
* Prevents the event from bubbling up if drag is in progress.
*/
FixedController.prototype.bypassSelection = function ($event) {
if (this.dragInProgress) {
if ($event) {
$event.stopPropagation();
}
return;
FixedController.prototype.clearSelection = function () {
if (this.selection) {
this.selection.deselect();
this.resizeHandles = [];
this.mvHandle = undefined;
}
};
@@ -575,38 +517,6 @@ define(
return this.mvHandle;
};
/**
* Gets the selection context.
*
* @param elementProxy the element proxy
* @returns {object} the context object which includes elementProxy and toolbar
*/
FixedController.prototype.getContext = function (elementProxy) {
return {
elementProxy: elementProxy,
toolbar: elementProxy
};
};
/**
* End drag.
*
* @param handle the resize handle
*/
FixedController.prototype.endDrag = function (handle) {
this.dragInProgress = true;
setTimeout(function () {
this.dragInProgress = false;
}.bind(this), 0);
if (handle) {
handle.endDrag();
} else {
this.moveHandle().endDrag();
}
};
return FixedController;
}
);

View File

@@ -65,7 +65,7 @@ define(
* Start a drag gesture. This should be called when a drag
* begins to track initial state.
*/
FixedDragHandle.prototype.startDrag = function () {
FixedDragHandle.prototype.startDrag = function startDrag() {
// Cache initial x/y positions
this.dragging = {
x: this.elementHandle.x(),

View File

@@ -27,11 +27,9 @@
*/
define(
[
'zepto',
'./LayoutDrag'
],
function (
$,
LayoutDrag
) {
@@ -52,12 +50,10 @@ define(
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function LayoutController($scope, $element, openmct) {
function LayoutController($scope) {
var self = this,
callbackCount = 0;
this.$element = $element;
// Update grid size when it changed
function updateGridSize(layoutGrid) {
var oldSize = self.gridSize;
@@ -127,11 +123,14 @@ define(
self.layoutPanels(ids);
self.setFrames(ids);
if (self.selectedId &&
self.selectedId !== $scope.domainObject.getId() &&
composition.indexOf(self.selectedId) === -1) {
// Click triggers selection of layout parent.
self.$element[0].click();
// If there is a newly-dropped object, select it.
if (self.droppedIdToSelectAfterRefresh) {
self.select(null, self.droppedIdToSelectAfterRefresh);
delete self.droppedIdToSelectAfterRefresh;
}
if (composition.indexOf(self.selectedId) === -1) {
self.clearSelection();
}
}
});
@@ -163,39 +162,22 @@ define(
}
};
// Sets the selectable object in response to the selection change event.
function setSelection(selectable) {
var selection = selectable[0];
if (!selection) {
delete self.selectedId;
return;
}
self.selectedId = selection.context.oldItem.getId();
self.drilledIn = undefined;
self.selectable = selectable;
}
this.positions = {};
this.rawPositions = {};
this.gridSize = DEFAULT_GRID_SIZE;
this.$scope = $scope;
this.drilledIn = undefined;
this.openmct = openmct;
// Watch for changes to the grid size in the model
$scope.$watch("model.layoutGrid", updateGridSize);
$scope.$watch("selection", function (selection) {
this.selection = selection;
}.bind(this));
// Update composed objects on screen, and position panes
$scope.$watchCollection("model.composition", refreshComposition);
openmct.selection.on('change', setSelection);
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
});
// Position panes where they are dropped
$scope.$on("mctDrop", handleDrop);
}
@@ -377,14 +359,37 @@ define(
};
/**
* Checks if the object is currently selected.
* Check if the object is currently selected.
*
* @param {string} obj the object to check for selection
* @returns {boolean} true if selected, otherwise false
*/
LayoutController.prototype.selected = function (obj) {
var sobj = this.openmct.selection.get()[0];
return (sobj && sobj.context.oldItem.getId() === obj.getId()) ? true : false;
return !!this.selectedId && this.selectedId === obj.getId();
};
/**
* Set the active user selection in this view.
*
* @param event the mouse event
* @param {string} id the object id
*/
LayoutController.prototype.select = function (event, id) {
if (event) {
event.stopPropagation();
if (this.selection) {
event.preventDefault();
}
}
this.selectedId = id;
var selectedObj = {};
selectedObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id);
if (this.selection) {
this.selection.select(selectedObj);
}
};
/**
@@ -393,7 +398,7 @@ define(
* @param {string} id the object id
* @private
*/
LayoutController.prototype.toggleFrame = function (id, domainObject) {
LayoutController.prototype.toggleFrame = function (id) {
var configuration = this.$scope.configuration;
if (!configuration.panels[id]) {
@@ -401,75 +406,21 @@ define(
}
this.frames[id] = configuration.panels[id].hasFrame = !this.frames[id];
var selection = this.openmct.selection.get();
selection[0].context.toolbar = this.getToolbar(id, domainObject);
this.openmct.selection.select(selection); // reselect so toolbar updates
this.select(undefined, id); // reselect so toolbar updates
};
/**
* Gets the toolbar object for the given domain object.
*
* @param id the domain object id
* @param domainObject the domain object
* @returns {object}
* @private
* Clear the current user selection.
*/
LayoutController.prototype.getToolbar = function (id, domainObject) {
var toolbarObj = {};
toolbarObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id, domainObject);
return toolbarObj;
};
/**
* Bypasses selection if drag is in progress.
*
* @param event the angular event object
*/
LayoutController.prototype.bypassSelection = function (event) {
LayoutController.prototype.clearSelection = function () {
if (this.dragInProgress) {
if (event) {
event.stopPropagation();
}
return;
}
};
/**
* Checks if the domain object is drilled in.
*
* @param domainObject the domain object
* @return true if the object is drilled in, false otherwise
*/
LayoutController.prototype.isDrilledIn = function (domainObject) {
return this.drilledIn === domainObject.getId();
};
/**
* Puts the given object in the drilled-in mode.
*
* @param event the angular event object
* @param domainObject the domain object
*/
LayoutController.prototype.drill = function (event, domainObject) {
if (event) {
event.stopPropagation();
}
if (!domainObject.getCapability('editor').inEditContext()) {
return;
}
if (!domainObject.hasCapability('composition')) {
return;
if (this.selection) {
this.selection.deselect();
delete this.selectedId;
}
// Disable since fixed position doesn't use the selection API yet
if (domainObject.getModel().type === 'telemetry.fixed') {
return;
}
this.drilledIn = domainObject.getId();
};
/**
@@ -491,36 +442,6 @@ define(
return this.gridSize;
};
/**
* Gets the selection context.
*
* @param domainObject the domain object
* @returns {object} the context object which includes
* item, oldItem and toolbar
*/
LayoutController.prototype.getContext = function (domainObject, toolbar) {
return {
item: domainObject.useCapability('adapter'),
oldItem: domainObject,
toolbar: toolbar ? this.getToolbar(domainObject.getId(), domainObject) : undefined
};
};
/**
* Selects a newly-dropped object.
*
* @param classSelector the css class selector
* @param domainObject the domain object
*/
LayoutController.prototype.selectIfNew = function (selector, domainObject) {
if (domainObject.getId() === this.droppedIdToSelectAfterRefresh) {
setTimeout(function () {
$('[data-layout-id="' + selector + '"]')[0].click();
delete this.droppedIdToSelectAfterRefresh;
}.bind(this), 0);
}
};
return LayoutController;
}
);

View File

@@ -55,8 +55,8 @@ define(
* @param element the fixed position element, as stored in its
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
* @param {number[]} gridSize the current layout grid size in [x,y] from
* @param {Array} elements the full array of elements
*/
function ElementProxy(element, index, elements, gridSize) {
/**

View File

@@ -21,14 +21,8 @@
*****************************************************************************/
define(
[
"../src/FixedController",
"zepto"
],
function (
FixedController,
$
) {
["../src/FixedController"],
function (FixedController) {
describe("The Fixed Position controller", function () {
var mockScope,
@@ -52,9 +46,6 @@ define(
mockMetadata,
mockTimeSystem,
mockLimitEvaluator,
mockSelection,
$element = [],
selectable = [],
controller;
// Utility function; find a watch for a given expression
@@ -187,36 +178,23 @@ define(
Promise.resolve(mockChildren)
);
mockScope.model = testModel;
mockScope.configuration = testConfiguration;
selectable[0] = {
context: {
oldItem: mockDomainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'select',
'on',
'off',
'get'
]);
mockSelection.get.andCallThrough();
mockScope.selection = jasmine.createSpyObj(
'selection',
['select', 'get', 'selected', 'deselect', 'proxy']
);
mockOpenMCT = {
time: mockConductor,
telemetry: mockTelemetryAPI,
composition: mockCompositionAPI,
selection: mockSelection
composition: mockCompositionAPI
};
$element = $('<div></div>');
spyOn($element[0], 'click');
mockMetadata = jasmine.createSpyObj('mockMetadata', [
'valuesForHints',
'value',
'values'
'value'
]);
mockMetadata.value.andReturn({
key: 'value'
@@ -248,11 +226,11 @@ define(
mockScope,
mockQ,
mockDialogService,
mockOpenMCT,
$element
mockOpenMCT
);
findWatch("model.layoutGrid")(testModel.layoutGrid);
findWatch("selection")(mockScope.selection);
});
it("subscribes when a domain object is available", function () {
@@ -328,41 +306,41 @@ define(
});
it("allows elements to be selected", function () {
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.isElementSelected()).toBe(true);
});
it("allows selection retrieval", function () {
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.getSelectedElement()).toEqual(elements[1]);
controller.select(elements[1]);
expect(mockScope.selection.select)
.toHaveBeenCalledWith(elements[1]);
});
it("selects the parent view when selected element is removed", function () {
it("allows selection retrieval", function () {
// selected with no arguments should give the current
// selection
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
var elements = controller.getElements();
selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
elements = controller.getElements();
controller.select(elements[1]);
mockScope.selection.get.andReturn(elements[1]);
expect(controller.selected()).toEqual(elements[1]);
});
elements[1].remove();
testModel.modified = 2;
it("allows selections to be cleared", function () {
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
expect($element[0].click).toHaveBeenCalled();
elements = controller.getElements();
controller.select(elements[1]);
controller.clearSelection();
expect(controller.selected(elements[1])).toBeFalsy();
});
it("retains selections during refresh", function () {
@@ -374,21 +352,23 @@ define(
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(elements[1]);
expect(controller.getSelectedElement()).toEqual(elements[1]);
// Verify precondition
expect(mockScope.selection.select.calls.length).toEqual(1);
// Mimic selection behavior
mockScope.selection.get.andReturn(elements[1]);
elements[2].remove();
testModel.modified = 2;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
// Verify removal, as test assumes this
expect(elements.length).toEqual(2);
expect(controller.shouldSelect(elements[1])).toBe(true);
expect(mockScope.selection.select.calls.length).toEqual(2);
});
it("Displays received values for telemetry elements", function () {
@@ -525,25 +505,21 @@ define(
});
it("exposes a view-level selection proxy", function () {
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var selection = mockOpenMCT.selection.select.mostRecentCall.args[0];
expect(mockOpenMCT.selection.select).toHaveBeenCalled();
expect(selection.context.viewProxy).toBeDefined();
expect(mockScope.selection.proxy).toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("exposes drag handles", function () {
var handles;
// Select something so that drag handles are expected
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(controller.getElements()[1]);
// Should have a non-empty array of handles
handles = controller.handles();
expect(handles).toEqual(jasmine.any(Array));
expect(handles.length).not.toEqual(0);
@@ -556,14 +532,15 @@ define(
});
it("exposes a move handle", function () {
var handle;
// Select something so that drag handles are expected
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(controller.getElements()[1]);
// Should have a move handle
var handle = controller.moveHandle();
handle = controller.moveHandle();
// And it should have start/continue/end drag methods
expect(handle.startDrag).toEqual(jasmine.any(Function));
@@ -574,40 +551,26 @@ define(
it("updates selection style during drag", function () {
var oldStyle;
// Select something so that drag handles are expected
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(controller.getElements()[1]);
mockScope.selection.get.andReturn(controller.getElements()[1]);
// Get style
oldStyle = controller.getSelectedElementStyle();
oldStyle = controller.selected().style;
// Start a drag gesture
controller.moveHandle().startDrag();
// Haven't moved yet; style shouldn't have updated yet
expect(controller.getSelectedElementStyle()).toEqual(oldStyle);
expect(controller.selected().style).toEqual(oldStyle);
// Drag a little
controller.moveHandle().continueDrag([1000, 100]);
// Style should have been updated
expect(controller.getSelectedElementStyle()).not.toEqual(oldStyle);
});
it("cleans up slection on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.mostRecentCall.args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
expect(controller.selected().style).not.toEqual(oldStyle);
});
describe("on display bounds changes", function () {
@@ -690,39 +653,6 @@ define(
});
});
it("selects an range value to display, if available", function () {
mockMetadata.valuesForHints.andReturn([
{
key: 'range',
source: 'range'
}
]);
var key = controller.chooseTelemetryKeyToDisplay(mockMetadata);
expect(key).toEqual('range');
});
it("selects the first non-domain value to display, if no range available", function () {
mockMetadata.valuesForHints.andReturn([]);
mockMetadata.values.andReturn([
{
key: 'domain',
source: 'domain',
hints: {
domain: 1
}
},
{
key: 'image',
source: 'image',
hints: {
image: 1
}
}
]);
var key = controller.chooseTelemetryKeyToDisplay(mockMetadata);
expect(key).toEqual('image');
});
it("reflects limit status", function () {
mockLimitEvaluator.evaluate.andReturn({cssClass: "alarm-a"});
controller.updateView(mockTelemetryObject, [{
@@ -739,14 +669,6 @@ define(
expect(controller.getElements()[0].cssClass).toEqual("alarm-a");
});
});
it("listens for selection change events", function () {
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
});
});
}

View File

@@ -21,14 +21,8 @@
*****************************************************************************/
define(
[
"../src/LayoutController",
"zepto"
],
function (
LayoutController,
$
) {
["../src/LayoutController"],
function (LayoutController) {
describe("The Layout controller", function () {
var mockScope,
@@ -38,12 +32,7 @@ define(
controller,
mockCompositionCapability,
mockComposition,
mockCompositionObjects,
mockOpenMCT,
mockSelection,
mockDomainObjectCapability,
$element = [],
selectable = [];
mockCompositionObjects;
function mockPromise(value) {
return {
@@ -69,18 +58,21 @@ define(
} else {
return {};
}
},
getCapability: function () {
return mockDomainObjectCapability;
},
hasCapability: function (param) {
if (param === 'composition') {
return id !== 'b';
}
}
};
}
// Utility function to find a watch for a given expression
function findWatch(expr) {
var watch;
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
watch = call.args[1];
}
});
return watch;
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
@@ -96,6 +88,7 @@ define(
mockComposition = ["a", "b", "c"];
mockCompositionObjects = mockComposition.map(mockDomainObject);
testConfiguration = {
panels: {
a: {
@@ -104,70 +97,27 @@ define(
}
}
};
mockDomainObjectCapability = jasmine.createSpyObj('capability',
['inEditContext']
);
mockCompositionCapability = mockPromise(mockCompositionObjects);
mockScope.domainObject = mockDomainObject("mockDomainObject");
mockScope.model = testModel;
mockScope.configuration = testConfiguration;
selectable[0] = {
context: {
oldItem: mockScope.domainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'select',
'on',
'off',
'get'
]);
mockSelection.get.andReturn(selectable);
mockOpenMCT = {
selection: mockSelection
};
$element = $('<div></div>');
$(document).find('body').append($element);
spyOn($element[0], 'click');
mockScope.selection = jasmine.createSpyObj(
'selection',
['select', 'get', 'selected', 'deselect']
);
spyOn(mockScope.domainObject, "useCapability").andCallThrough();
controller = new LayoutController(mockScope, $element, mockOpenMCT);
controller = new LayoutController(mockScope);
spyOn(controller, "layoutPanels").andCallThrough();
findWatch("selection")(mockScope.selection);
jasmine.Clock.useMock();
});
afterEach(function () {
$element.remove();
});
it("listens for selection change events", function () {
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
it("cleans up on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.calls[0].args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
// Model changes will indicate that panel positions
// may have changed, for instance.
it("watches for changes to composition", function () {
@@ -370,35 +320,67 @@ define(
.not.toEqual(oldStyle);
});
it("allows objects to be selected", function () {
it("allows panels to be selected", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(mockEvent, childObj.getId());
expect(mockEvent.stopPropagation).toHaveBeenCalled();
expect(controller.selected(childObj)).toBe(true);
});
it("prevents event bubbling while drag is in progress", function () {
it("allows selection to be cleared", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
controller.select(null, childObj.getId());
controller.clearSelection();
expect(controller.selected(childObj)).toBeFalsy();
});
it("prevents clearing selection while drag is in progress", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
var id = childObj.getId();
controller.select(mockEvent, id);
// Do a drag
controller.startDrag(childObj.getId(), [1, 1], [0, 0]);
controller.startDrag(id, [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
// Because mouse position could cause the parent object to be selected, this should be ignored.
controller.bypassSelection(mockEvent);
// Because mouse position could cause clearSelection to be called, this should be ignored.
controller.clearSelection();
expect(mockEvent.stopPropagation).toHaveBeenCalled();
expect(controller.selected(childObj)).toBe(true);
// Shoud be able to select another object when dragging is done.
// Shoud be able to clear the selection after dragging is done.
jasmine.Clock.tick(0);
mockEvent.stopPropagation.reset();
controller.bypassSelection(mockEvent);
controller.clearSelection();
expect(mockEvent.stopPropagation).not.toHaveBeenCalled();
expect(controller.selected(childObj)).toBe(false);
});
it("clears selection after moving/resizing", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
var id = childObj.getId();
controller.select(mockEvent, id);
// Do a drag
controller.startDrag(id, [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
jasmine.Clock.tick(0);
controller.clearSelection();
expect(controller.selected(childObj)).toBe(false);
});
it("shows frames by default", function () {
@@ -416,74 +398,43 @@ define(
it("hides frame when selected object has frame ", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
controller.select(mockEvent, childObj.getId());
expect(mockScope.selection.select).toHaveBeenCalled();
var selectedObj = mockScope.selection.select.mostRecentCall.args[0];
expect(controller.hasFrame(childObj)).toBe(true);
expect(toolbarObj.hideFrame).toBeDefined();
expect(toolbarObj.hideFrame).toEqual(jasmine.any(Function));
expect(selectedObj.hideFrame).toBeDefined();
expect(selectedObj.hideFrame).toEqual(jasmine.any(Function));
});
it("shows frame when selected object has no frame", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[1];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
controller.select(mockEvent, childObj.getId());
expect(mockScope.selection.select).toHaveBeenCalled();
var selectedObj = mockScope.selection.select.mostRecentCall.args[0];
expect(controller.hasFrame(childObj)).toBe(false);
expect(toolbarObj.showFrame).toBeDefined();
expect(toolbarObj.showFrame).toEqual(jasmine.any(Function));
expect(selectedObj.showFrame).toBeDefined();
expect(selectedObj.showFrame).toEqual(jasmine.any(Function));
});
it("selects the parent object when selected object is removed", function () {
it("deselects the object that is no longer in the composition", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(mockEvent, childObj.getId());
var composition = ["b", "c"];
mockScope.$watchCollection.mostRecentCall.args[1](composition);
expect($element[0].click).toHaveBeenCalled();
expect(controller.selected(childObj)).toBe(false);
});
it("allows objects to be drilled-in only when editing", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
childObj.getCapability().inEditContext.andReturn(false);
controller.drill(mockEvent, childObj);
expect(controller.isDrilledIn(childObj)).toBe(false);
});
it("allows objects to be drilled-in only if it has sub objects", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[1];
childObj.getCapability().inEditContext.andReturn(true);
controller.drill(mockEvent, childObj);
expect(controller.isDrilledIn(childObj)).toBe(false);
});
it("selects a newly-dropped object", function () {
mockScope.$on.mostRecentCall.args[1](
mockEvent,
'd',
{ x: 300, y: 100 }
);
var childObj = mockDomainObject("d");
var testElement = $("<div data-layout-id='some-id'></div>");
$element.append(testElement);
spyOn(testElement[0], 'click');
controller.selectIfNew('some-id', childObj);
jasmine.Clock.tick(0);
expect(testElement[0].click).toHaveBeenCalled();
});
});
}
);

View File

@@ -43,7 +43,7 @@ define(
* @param {constant} EXPORT_IMAGE_TIMEOUT time in milliseconds before a timeout error is returned
* @constructor
*/
function ExportImageService($q, $timeout, $log, EXPORT_IMAGE_TIMEOUT, injHtml2Canvas, injSaveAs, injFileReader, injChangeBackgroundColor) {
function ExportImageService($q, $timeout, $log, EXPORT_IMAGE_TIMEOUT, injHtml2Canvas, injSaveAs, injFileReader) {
self.$q = $q;
self.$timeout = $timeout;
self.$log = $log;
@@ -51,7 +51,6 @@ define(
self.html2canvas = injHtml2Canvas || html2canvas;
self.saveAs = injSaveAs || saveAs;
self.reader = injFileReader || new FileReader();
self.changeBackgroundColor = injChangeBackgroundColor || self.changeBackgroundColor;
}
/**
@@ -64,20 +63,13 @@ define(
function renderElement(element, type) {
var defer = self.$q.defer(),
validTypes = ["png", "jpg", "jpeg"],
renderTimeout,
originalColor;
renderTimeout;
if (validTypes.indexOf(type) === -1) {
self.$log.error("Invalid type requested. Try: (" + validTypes.join(",") + ")");
return;
}
// Save color to be restored later
originalColor = element.style.backgroundColor || '';
// Defaulting to white so we can see the chart when printed
self.changeBackgroundColor(element, 'white');
renderTimeout = self.$timeout(function () {
defer.reject("html2canvas timed out");
self.$log.warn("html2canvas timed out");
@@ -86,13 +78,13 @@ define(
try {
self.html2canvas(element, {
onrendered: function (canvas) {
self.changeBackgroundColor(element, originalColor);
switch (type.toLowerCase()) {
case "png":
canvas.toBlob(defer.resolve, "image/png");
break;
default:
case "jpg":
case "jpeg":
canvas.toBlob(defer.resolve, "image/jpeg");
break;
@@ -104,10 +96,7 @@ define(
self.$log.warn("html2canvas failed with error: " + e);
}
defer.promise.finally(function () {
renderTimeout.cancel();
self.changeBackgroundColor(element, originalColor);
});
defer.promise.finally(renderTimeout.cancel);
return defer.promise;
}
@@ -136,13 +125,6 @@ define(
}
}
/**
* @private
*/
self.changeBackgroundColor = function (element, color) {
element.style.backgroundColor = color;
};
/**
* Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported

View File

@@ -37,8 +37,7 @@ define(
mockFileReader,
mockExportTimeoutConstant,
testElement,
exportImageService,
mockChangeBackgroundColor;
exportImageService;
describe("ExportImageService", function () {
beforeEach(function () {
@@ -84,9 +83,7 @@ define(
["readAsDataURL", "onloadend"]
);
mockExportTimeoutConstant = 0;
testElement = {style: {backgroundColor: 'black'}};
mockChangeBackgroundColor = jasmine.createSpy('changeBackgroundColor');
testElement = {};
exportImageService = new ExportImageService(
mockQ,
@@ -95,8 +92,7 @@ define(
mockExportTimeoutConstant,
mockHtml2Canvas,
mockSaveAs,
mockFileReader,
mockChangeBackgroundColor
mockFileReader
);
});
@@ -119,18 +115,6 @@ define(
expect(mockSaveAs).toHaveBeenCalled();
expect(mockPromise.finally).toHaveBeenCalled();
});
it("changes background color to white and returns color back to original after snapshot, for better visibility of plot lines on print", function () {
exportImageService.exportPNG(testElement, "plot.png");
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'white');
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'black');
exportImageService.exportJPG(testElement, "plot.jpg");
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'white');
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'black');
});
});
}
);

View File

@@ -69,7 +69,7 @@ define([
"delegates": [
"telemetry"
],
"inspector": "table-options-edit",
"inspector": tableInspector,
"contains": [
{
"has": "telemetry"

View File

@@ -19,10 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-if="domainObject.getCapability('editor').inEditContext()"
ng-controller="TableOptionsController"
class="l-controls-first flex-elem grows l-inspector-part">
<div ng-controller="TableOptionsController" class="l-controls-first flex-elem grows l-inspector-part">
<em class="t-inspector-part-header" title="Display properties for this object">Table Options</em>
<mct-form
ng-model="configuration.table.columns"

View File

@@ -32,7 +32,6 @@ define(
*/
function TelemetryCollection() {
EventEmitter.call(this, arguments);
this.dupeCheck = false;
this.telemetry = [];
this.highBuffer = [];
this.sortField = undefined;
@@ -162,7 +161,7 @@ define(
var startIx = _.sortedIndex(array, item, this.sortField);
var endIx;
if (this.dupeCheck && startIx !== array.length) {
if (startIx !== array.length) {
endIx = _.sortedLastIndex(array, item, this.sortField);
// Create an array of potential dupes, based on having the
@@ -190,7 +189,6 @@ define(
TelemetryCollection.prototype.add = function (items) {
var added = items.filter(this.addOne);
this.emit('added', added);
this.dupeCheck = true;
};
/**

View File

@@ -436,31 +436,9 @@ define(
* @param {Object} searchElement Object to find the insertion point for
*/
MCTTableController.prototype.findInsertionPoint = function (searchArray, searchElement) {
var index;
var testIndex;
var first = searchArray[0];
var last = searchArray[searchArray.length - 1];
if (first) {
first = first[this.$scope.sortColumn].text;
}
if (last) {
last = last[this.$scope.sortColumn].text;
}
// Shortcut check for append/prepend
if (first && this.sortComparator(first, searchElement) >= 0) {
index = testIndex = 0;
} else if (last && this.sortComparator(last, searchElement) <= 0) {
index = testIndex = searchArray.length;
} else {
// use a binary search to find the correct insertion point
index = testIndex = this.binarySearch(
searchArray,
searchElement,
0,
searchArray.length - 1
);
}
//First, use a binary search to find the correct insertion point
var index = this.binarySearch(searchArray, searchElement, 0, searchArray.length - 1);
var testIndex = index;
//It's possible that the insertion point is a duplicate of the element to be inserted
var isDupe = function () {

View File

@@ -170,9 +170,6 @@ define(
* @param rows
*/
TelemetryTableController.prototype.addRowsToTable = function (rows) {
rows.forEach(function (row) {
this.$scope.rows.push(row);
}, this);
this.$scope.$broadcast('add:rows', rows);
};

View File

@@ -436,28 +436,5 @@ define(
expect(mockScope.$broadcast).toHaveBeenCalledWith("remove:rows", discardedRows);
});
describe('when telemetry is added', function () {
var testRows;
var expectedRows;
beforeEach(function () {
testRows = [{ a: 0 }, { a: 1 }, { a: 2 }];
mockScope.rows = [{ a: -1 }];
expectedRows = mockScope.rows.concat(testRows);
spyOn(controller.telemetry, "on").andCallThrough();
controller.registerChangeListeners();
controller.telemetry.on.calls.forEach(function (call) {
if (call.args[0] === 'added') {
call.args[1](testRows);
}
});
});
it("adds it to rows in scope", function () {
expect(mockScope.rows).toEqual(expectedRows);
});
});
});
});

View File

@@ -38,7 +38,6 @@ define([
"./src/capabilities/CostCapability",
"./src/directives/MCTSwimlaneDrop",
"./src/directives/MCTSwimlaneDrag",
"./src/directives/MCTResourceGraphDrop",
"./src/services/ObjectLoader",
"./src/chart/MCTTimelineChart",
"text!./res/templates/values.html",
@@ -70,7 +69,6 @@ define([
CostCapability,
MCTSwimlaneDrop,
MCTSwimlaneDrag,
MCTResourceGraphDrop,
ObjectLoader,
MCTTimelineChart,
valuesTemplate,
@@ -579,13 +577,6 @@ define([
"$interval",
"$log"
]
},
{
"key": "mctResourceGraphDrop",
"implementation": MCTResourceGraphDrop,
"depends": [
"dndService"
]
}
],
"services": [

View File

@@ -38,10 +38,6 @@
.l-timeline-pane {
@include absPosDefault();
&.drop-over {
background-color: lighten($colorEditAreaBg, 5%);
}
.l-width-control {
position: relative;
}

View File

@@ -20,7 +20,6 @@
at runtime from the About dialog for additional information.
-->
<div class="s-timeline l-timeline-holder split-layout vertical splitter-sm"
ng-click="$event.stopPropagation()"
ng-controller="TimelineController as timelineController">
<mct-split-pane anchor="left" class="abs" position="pane.x">
@@ -78,8 +77,7 @@
<mct-splitter></mct-splitter>
<!-- BOTTOM PANE RESOURCE LEGEND -->
<div mct-resource-graph-drop
class="split-pane-component abs l-timeline-pane t-pane-h l-pane-btm s-timeline-resource-legend l-timeline-resource-legend">
<div class="split-pane-component abs l-timeline-pane t-pane-h l-pane-btm s-timeline-resource-legend l-timeline-resource-legend">
<div class="l-title s-title">{{ngModel.title}}Resource Graph Legend</div>
<div class="l-legend-items legend">
<mct-include key="'timeline-legend-item'"

View File

@@ -1,81 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['./SwimlaneDragConstants'],
function (SwimlaneDragConstants) {
/**
* Defines the `mct-resource-graph-drop` directive. When a drop occurs
* on an element with this attribute, the swimlane targeted by the drop
* will receive the dropped domain object (at which point it can handle
* the drop, typically by toggling the swimlane graph.)
* @param {DndService} dndService drag-and-drop service
*/
function MCTResourceGraphDrop(dndService) {
function link(scope, element, attrs) {
// Handle dragover
element.on('dragover', function (e) {
var swimlane = dndService.getData(
SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE
);
if (typeof swimlane !== "undefined" && !swimlane.graph()) {
element.addClass('drop-over');
scope.$apply();
e.preventDefault();
}
});
// Handle drops
element.on('drop', function (e) {
var swimlane = dndService.getData(
SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE
);
element.removeClass('drop-over');
// Only toggle if the graph isn't already set
if (typeof swimlane !== "undefined" && !swimlane.graph()) {
swimlane.toggleGraph();
e.preventDefault();
}
});
// Clear highlights when drag leaves this swimlane
element.on('dragleave', function (e) {
element.removeClass('drop-over');
scope.$apply();
e.preventDefault();
});
}
return {
// Applies to attributes
restrict: "A",
// Link using above function
link: link
};
}
return MCTResourceGraphDrop;
}
);

View File

@@ -1,159 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['../../src/directives/MCTResourceGraphDrop', '../../src/directives/SwimlaneDragConstants'],
function (MCTResourceGraphDrop, SwimlaneDragConstants) {
describe("The mct-resource-graph-drop directive", function () {
var mockDndService,
mockScope,
mockElement,
testAttrs,
mockSwimlane,
testEvent,
handlers,
directive;
beforeEach(function () {
handlers = {};
mockDndService = jasmine.createSpyObj(
'dndService',
['setData', 'getData', 'removeData']
);
mockScope = jasmine.createSpyObj('$scope', ['$eval', '$apply']);
mockElement = jasmine.createSpyObj('element', ['on', 'addClass', 'removeClass']);
testAttrs = { mctSwimlaneDrop: "mockSwimlane" };
mockSwimlane = jasmine.createSpyObj(
"swimlane",
['graph', 'toggleGraph']
);
testEvent = {
dataTransfer: { getData: jasmine.createSpy() },
preventDefault: jasmine.createSpy(),
stopPropagation: jasmine.createSpy()
};
testEvent.dataTransfer.getData.andReturn('abc');
mockDndService.getData.andCallFake(function (key) {
return key === SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE ?
mockSwimlane : undefined;
});
mockSwimlane.graph.andReturn(false);
directive = new MCTResourceGraphDrop(mockDndService);
directive.link(mockScope, mockElement, testAttrs);
mockElement.on.calls.forEach(function (call) {
handlers[call.args[0]] = call.args[1];
});
});
it("is available as an attribute", function () {
expect(directive.restrict).toEqual("A");
});
[false, true].forEach(function (graphing) {
describe("when swimlane graph is " + (graphing ? "" : "not ") + "enabled", function () {
beforeEach(function () {
mockSwimlane.graph.andReturn(graphing);
});
describe("on dragover", function () {
var prefix = !graphing ? "does" : "does not";
beforeEach(function () {
handlers.dragover(testEvent);
});
it(prefix + " add a drop-over class", function () {
var expectAddClass = expect(mockElement.addClass);
(!graphing ? expectAddClass : expectAddClass.not)
.toHaveBeenCalledWith('drop-over');
});
it(prefix + " call $apply on scope", function () {
var expectApply = expect(mockScope.$apply);
(!graphing ? expectApply : expectApply.not)
.toHaveBeenCalled();
});
it(prefix + " prevent default", function () {
var expectPreventDefault = expect(testEvent.preventDefault);
(!graphing ? expectPreventDefault : expectPreventDefault.not)
.toHaveBeenCalled();
});
});
describe("on drop", function () {
var prefix = !graphing ? "does" : "does not";
beforeEach(function () {
handlers.drop(testEvent);
});
it("removes any drop-over class", function () {
expect(mockElement.removeClass)
.toHaveBeenCalledWith('drop-over');
});
it(prefix + " toggle the swimlane's resource graph", function () {
var expectToggle = expect(mockSwimlane.toggleGraph);
(!graphing ? expectToggle : expectToggle.not)
.toHaveBeenCalled();
});
it(prefix + " prevent default", function () {
var expectPreventDefault = expect(testEvent.preventDefault);
(!graphing ? expectPreventDefault : expectPreventDefault.not)
.toHaveBeenCalled();
});
});
describe("on dragleave", function () {
beforeEach(function () {
handlers.dragleave(testEvent);
});
it("removes any drop-over class", function () {
expect(mockElement.removeClass)
.toHaveBeenCalledWith('drop-over');
});
it("calls $apply on scope", function () {
expect(mockScope.$apply).toHaveBeenCalled();
});
it("calls preventDefault on events", function () {
expect(testEvent.preventDefault).toHaveBeenCalled();
});
});
});
});
});
}
);

View File

@@ -28,8 +28,7 @@ define([
'./selection/Selection',
'./api/objects/object-utils',
'./plugins/plugins',
'./ui/ViewRegistry',
'./ui/InspectorViewRegistry'
'./ui/ViewRegistry'
], function (
EventEmitter,
legacyRegistry,
@@ -38,8 +37,7 @@ define([
Selection,
objectUtils,
plugins,
ViewRegistry,
InspectorViewRegistry
ViewRegistry
) {
/**
* Open MCT is an extensible web application for building mission
@@ -114,13 +112,15 @@ define([
/**
* Registry for views which should appear in the Inspector area.
* These views will be chosen based on the selection state.
* These views will be chosen based on selection state, so
* providers should be prepared to test arbitrary objects for
* viewability.
*
* @type {module:openmct.InspectorViewRegistry}
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name inspectorViews
* @name inspectors
*/
this.inspectorViews = new InspectorViewRegistry();
this.inspectors = new ViewRegistry();
/**
* Registry for views which should appear in Edit Properties
@@ -196,6 +196,7 @@ define([
this.Dialog = api.Dialog;
this.on('navigation', this.selection.clear.bind(this.selection));
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@@ -48,7 +48,6 @@ define([
this.unlisteners.forEach(function (unlisten) {
unlisten();
});
this.unlisteners = [];
};
/**

View File

@@ -50,18 +50,11 @@ define([
this.rootProvider = new RootObjectProvider(this.rootRegistry);
}
/**
* Set fallback provider, this is an internal API for legacy reasons.
* @private
*/
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
this.fallbackProvider = p;
};
/**
* Retrieve the provider for a given identifier.
* @private
*/
// Retrieve the provider for a given key.
ObjectAPI.prototype.getProvider = function (identifier) {
if (identifier.key === 'ROOT') {
return this.rootProvider;
@@ -142,28 +135,27 @@ define([
* @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved
*/
ObjectAPI.prototype.get = function (identifier) {
identifier = utils.parseKeyString(identifier);
var provider = this.getProvider(identifier);
if (!provider) {
throw new Error('No Provider Matched');
}
[
'save',
'delete',
'get'
].forEach(function (method) {
ObjectAPI.prototype[method] = function () {
var identifier = arguments[0],
provider = this.getProvider(identifier);
if (!provider.get) {
throw new Error('Provider does not support get!');
}
if (!provider) {
throw new Error('No Provider Matched');
}
return provider.get(identifier);
};
if (!provider[method]) {
throw new Error('Provider does not support [' + method + '].');
}
ObjectAPI.prototype.delete = function () {
throw new Error('Delete not implemented');
};
ObjectAPI.prototype.save = function () {
throw new Error('Save not implemented');
};
return provider[method].apply(provider, arguments);
};
});
/**
* Add a root-level object.

View File

@@ -41,6 +41,8 @@ define([
};
this.valueMetadata = valueMetadata;
this.parseCache = new WeakMap();
this.formatCache = new WeakMap();
try {
this.formatter = formatService
.getFormat(valueMetadata.format, valueMetadata);
@@ -70,14 +72,26 @@ define([
TelemetryValueFormatter.prototype.parse = function (datum) {
if (_.isObject(datum)) {
return this.formatter.parse(datum[this.valueMetadata.source]);
if (!this.parseCache.has(datum)) {
this.parseCache.set(
datum,
this.formatter.parse(datum[this.valueMetadata.source])
);
}
return this.parseCache.get(datum);
}
return this.formatter.parse(datum);
};
TelemetryValueFormatter.prototype.format = function (datum) {
if (_.isObject(datum)) {
return this.formatter.format(datum[this.valueMetadata.source]);
if (!this.formatCache.has(datum)) {
this.formatCache.set(
datum,
this.formatter.format(datum[this.valueMetadata.source])
);
}
return this.formatCache.get(datum);
}
return this.formatter.format(datum);
};

View File

@@ -1,97 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
// Set of connection states; changing among these states will be
// reflected in the indicator's appearance.
// CONNECTED: Everything nominal, expect to be able to read/write.
// DISCONNECTED: HTTP failed; maybe misconfigured, disconnected.
// PENDING: Still trying to connect, and haven't failed yet.
var CONNECTED = {
glyphClass: "ok"
},
PENDING = {
glyphClass: 'caution'
},
DISCONNECTED = {
glyphClass: "err"
};
function URLIndicator($http, $interval) {
var self = this;
this.cssClass = this.options.cssClass ? this.options.cssClass : "icon-chain-links";
this.URLpath = this.options.url;
this.label = this.options.label ? this.options.label : this.options.url;
this.interval = this.options.interval || 10000;
this.state = PENDING;
function handleError(e) {
self.state = DISCONNECTED;
}
function handleResponse() {
self.state = CONNECTED;
}
function updateIndicator() {
$http.get(self.URLpath).then(handleResponse, handleError);
}
updateIndicator();
$interval(updateIndicator, self.interval, 0, false);
}
URLIndicator.prototype.getCssClass = function () {
return this.cssClass;
};
URLIndicator.prototype.getGlyphClass = function () {
return this.state.glyphClass;
};
URLIndicator.prototype.getText = function () {
switch (this.state) {
case CONNECTED: {
return this.label + " is connected";
}
case PENDING: {
return "Checking status of " + this.label + " please stand by...";
}
case DISCONNECTED: {
return this.label + " is offline";
}
}
};
URLIndicator.prototype.getDescription = function () {
switch (this.state) {
case CONNECTED: {
return this.label + " is online, checking status every " +
this.interval + " milliseconds.";
}
case PENDING: {
return "Checking status of " + this.label + " please stand by...";
}
case DISCONNECTED: {
return this.label + " is offline, checking status every " +
this.interval + " milliseconds";
}
}
};
return URLIndicator;
});

View File

@@ -1,20 +0,0 @@
define(
[
'./URLIndicator'
],
function URLIndicatorPlugin(URLIndicator) {
return function (opts) {
// Wrap the plugin in a function so we can apply the arguments.
function URLIndicatorWrapper() {
this.options = opts;
URLIndicator.apply(this, arguments);
}
URLIndicatorWrapper.prototype = Object.create(URLIndicator.prototype);
return function install(openmct) {
openmct.legacyExtension('indicators', {
"implementation": URLIndicatorWrapper,
"depends": ["$http", "$interval"]
});
};
};
});

View File

@@ -1,158 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["./URLIndicator"],
function (URLIndicator) {
describe("The URLIndicator", function () {
var mockHttp,
mockInterval,
mockPromise,
opts,
Indicator,
indicatorWrapper;
beforeEach(function () {
mockHttp = jasmine.createSpyObj("$http", ["get"]);
mockInterval = jasmine.createSpy("$interval");
mockPromise = jasmine.createSpyObj("promise", ["then"]);
opts = {
url: "http://localhost:8080",
interval: 1337 //some number
};
mockHttp.get.andReturn(mockPromise);
Indicator = function () {
this.options = opts;
URLIndicator.call(this, mockHttp, mockInterval);
};
Indicator.prototype = Object.create(URLIndicator.prototype);
indicatorWrapper = new Indicator();
});
it("polls for changes", function () {
expect(mockInterval).toHaveBeenCalledWith(
jasmine.any(Function),
opts.interval,
0,
false
);
});
it("has a database cssClass as default", function () {
expect(indicatorWrapper.getCssClass()).toEqual("icon-database");
});
it("consults the url with the path supplied", function () {
expect(mockHttp.get).toHaveBeenCalledWith(opts.url);
});
it("changes when the database connection is nominal", function () {
var initialText = indicatorWrapper.getText(),
initialDescrption = indicatorWrapper.getDescription(),
initialGlyphClass = indicatorWrapper.getGlyphClass();
// Nominal just means getting back an object, without
// an error field.
mockPromise.then.mostRecentCall.args[0]({ data: {} });
// Verify that these values changed;
// don't test for specific text.
expect(indicatorWrapper.getText()).not.toEqual(initialText);
expect(indicatorWrapper.getGlyphClass()).not.toEqual(initialGlyphClass);
expect(indicatorWrapper.getDescription()).not.toEqual(initialDescrption);
// Do check for specific class
expect(indicatorWrapper.getGlyphClass()).toEqual("ok");
});
it("changes when the server cannot be reached", function () {
var initialText = indicatorWrapper.getText(),
initialDescrption = indicatorWrapper.getDescription(),
initialGlyphClass = indicatorWrapper.getGlyphClass();
// Nominal just means getting back an object, without
// an error field.
mockPromise.then.mostRecentCall.args[1]({ data: {} });
// Verify that these values changed;
// don't test for specific text.
expect(indicatorWrapper.getText()).not.toEqual(initialText);
expect(indicatorWrapper.getGlyphClass()).not.toEqual(initialGlyphClass);
expect(indicatorWrapper.getDescription()).not.toEqual(initialDescrption);
// Do check for specific class
expect(indicatorWrapper.getGlyphClass()).toEqual("err");
});
it("has a customized cssClass if supplied in initialization", function () {
opts = {
url: "http://localhost:8080",
cssClass: "cssClass-checked",
interval: 10000
};
indicatorWrapper = new Indicator();
expect(indicatorWrapper.getCssClass()).toEqual("cssClass-checked");
});
it("has a customized interval if supplied in initialization", function () {
opts = {
url: "http://localhost:8080",
interval: 1814
};
indicatorWrapper = new Indicator();
expect(mockInterval).toHaveBeenCalledWith(
jasmine.any(Function),
1814,
0,
false
);
});
it("has a custom label if supplied in initialization", function () {
opts = {
url: "http://localhost:8080",
label: "Localhost"
};
indicatorWrapper = new Indicator();
expect(indicatorWrapper.getText()).toEqual("Checking status of Localhost please stand by...");
});
it("has a default label if not supplied in initialization", function () {
opts = {
url: "http://localhost:8080"
};
indicatorWrapper = new Indicator();
expect(indicatorWrapper.getText()).toEqual(
"Checking status of http://localhost:8080 please stand by..."
);
});
it("has a default interval if not supplied in initialization", function () {
opts = {
url: "http://localhost:8080"
};
indicatorWrapper = new Indicator();
expect(mockInterval).toHaveBeenCalledWith(
jasmine.any(Function),
10000,
0,
false
);
});
});
}
);

View File

@@ -1,34 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* Constant values used by the Autoflow Tabular View.
*/
return {
ROW_HEIGHT: 16,
SLIDER_HEIGHT: 10,
INITIAL_COLUMN_WIDTH: 225,
MAX_COLUMN_WIDTH: 525,
COLUMN_WIDTH_STEP: 25
};
});

View File

@@ -1,121 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./AutoflowTabularRowController'
], function (AutoflowTabularRowController) {
/**
* Controller for an Autoflow Tabular View. Subscribes to telemetry
* associated with children of the domain object and passes that
* information on to the view.
*
* @param {DomainObject} domainObject the object being viewed
* @param {*} data the view data
* @param openmct a reference to the openmct application
*/
function AutoflowTabularController(domainObject, data, openmct) {
this.composition = openmct.composition.get(domainObject);
this.data = data;
this.openmct = openmct;
this.rows = {};
this.controllers = {};
this.addRow = this.addRow.bind(this);
this.removeRow = this.removeRow.bind(this);
}
/**
* Set the "Last Updated" value to be displayed.
* @param {String} value the value to display
* @private
*/
AutoflowTabularController.prototype.trackLastUpdated = function (value) {
this.data.updated = value;
};
/**
* Respond to an `add` event from composition by adding a new row.
* @private
*/
AutoflowTabularController.prototype.addRow = function (childObject) {
var identifier = childObject.identifier;
var id = [identifier.namespace, identifier.key].join(":");
if (!this.rows[id]) {
this.rows[id] = {
classes: "",
name: childObject.name,
value: undefined
};
this.controllers[id] = new AutoflowTabularRowController(
childObject,
this.rows[id],
this.openmct,
this.trackLastUpdated.bind(this)
);
this.controllers[id].activate();
this.data.items.push(this.rows[id]);
}
};
/**
* Respond to an `remove` event from composition by removing any
* related row.
* @private
*/
AutoflowTabularController.prototype.removeRow = function (identifier) {
var id = [identifier.namespace, identifier.key].join(":");
if (this.rows[id]) {
this.data.items = this.data.items.filter(function (item) {
return item !== this.rows[id];
}.bind(this));
this.controllers[id].destroy();
delete this.controllers[id];
delete this.rows[id];
}
};
/**
* Activate this controller; begin listening for changes.
*/
AutoflowTabularController.prototype.activate = function () {
this.composition.on('add', this.addRow);
this.composition.on('remove', this.removeRow);
this.composition.load();
};
/**
* Destroy this controller; detach any associated resources.
*/
AutoflowTabularController.prototype.destroy = function () {
Object.keys(this.controllers).forEach(function (id) {
this.controllers[id].destroy();
}.bind(this));
this.controllers = {};
this.composition.off('add', this.addRow);
this.composition.off('remove', this.removeRow);
};
return AutoflowTabularController;
});

View File

@@ -1,60 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./AutoflowTabularView'
], function (
AutoflowTabularView
) {
/**
* This plugin provides an Autoflow Tabular View for domain objects
* in Open MCT.
*
* @param {Object} options
* @param {String} [options.type] the domain object type for which
* this view should be available; if omitted, this view will
* be available for all objects
*/
return function (options) {
return function (openmct) {
var views = (openmct.mainViews || openmct.objectViews);
views.addProvider({
name: "Autoflow Tabular",
key: "autoflow",
cssClass: "icon-packet",
description: "A tabular view of packet contents.",
canView: function (d) {
return !options || (options.type === d.type);
},
view: function (domainObject) {
return new AutoflowTabularView(
domainObject,
openmct,
document
);
}
});
};
};
});

View File

@@ -1,319 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./AutoflowTabularPlugin',
'./AutoflowTabularConstants',
'../../MCT',
'zepto'
], function (AutoflowTabularPlugin, AutoflowTabularConstants, MCT, $) {
describe("AutoflowTabularPlugin", function () {
var testType;
var testObject;
var mockmct;
beforeEach(function () {
testType = "some-type";
testObject = { type: testType };
mockmct = new MCT();
spyOn(mockmct.composition, 'get');
spyOn(mockmct.objectViews, 'addProvider');
spyOn(mockmct.telemetry, 'getMetadata');
spyOn(mockmct.telemetry, 'getValueFormatter');
spyOn(mockmct.telemetry, 'limitEvaluator');
spyOn(mockmct.telemetry, 'request');
spyOn(mockmct.telemetry, 'subscribe');
var plugin = new AutoflowTabularPlugin({ type: testType });
plugin(mockmct);
});
it("installs a view provider", function () {
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
});
describe("installs a view provider which", function () {
var provider;
beforeEach(function () {
provider =
mockmct.objectViews.addProvider.mostRecentCall.args[0];
});
it("applies its view to the type from options", function () {
expect(provider.canView(testObject)).toBe(true);
});
it("does not apply to other types", function () {
expect(provider.canView({ type: 'foo' })).toBe(false);
});
describe("provides a view which", function () {
var testKeys;
var testChildren;
var testContainer;
var testHistories;
var mockComposition;
var mockMetadata;
var mockEvaluator;
var mockUnsubscribes;
var callbacks;
var view;
function waitsForChange() {
var callback = jasmine.createSpy('callback');
window.requestAnimationFrame(callback);
waitsFor(function () {
return callback.calls.length > 0;
});
}
function emitEvent(mockEmitter, type, event) {
mockEmitter.on.calls.forEach(function (call) {
if (call.args[0] === type) {
call.args[1](event);
}
});
}
beforeEach(function () {
callbacks = {};
testObject = { type: 'some-type' };
testKeys = ['abc', 'def', 'xyz'];
testChildren = testKeys.map(function (key) {
return {
identifier: { namespace: "test", key: key },
name: "Object " + key
};
});
testContainer = $('<div>')[0];
testHistories = testKeys.reduce(function (histories, key, index) {
histories[key] = { key: key, range: index + 10, domain: key + index };
return histories;
}, {});
mockComposition =
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
mockMetadata =
jasmine.createSpyObj('metadata', ['valuesForHints']);
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
mockUnsubscribes = testKeys.reduce(function (map, key) {
map[key] = jasmine.createSpy('unsubscribe-' + key);
return map;
}, {});
mockmct.composition.get.andReturn(mockComposition);
mockComposition.load.andCallFake(function () {
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
return Promise.resolve(testChildren);
});
mockmct.telemetry.getMetadata.andReturn(mockMetadata);
mockmct.telemetry.getValueFormatter.andCallFake(function (metadatum) {
var mockFormatter = jasmine.createSpyObj('formatter', ['format']);
mockFormatter.format.andCallFake(function (datum) {
return datum[metadatum.hint];
});
return mockFormatter;
});
mockmct.telemetry.limitEvaluator.andReturn(mockEvaluator);
mockmct.telemetry.subscribe.andCallFake(function (obj, callback) {
var key = obj.identifier.key;
callbacks[key] = callback;
return mockUnsubscribes[key];
});
mockmct.telemetry.request.andCallFake(function (obj, request) {
var key = obj.identifier.key;
return Promise.resolve([testHistories[key]]);
});
mockMetadata.valuesForHints.andCallFake(function (hints) {
return [{ hint: hints[0] }];
});
view = provider.view(testObject);
view.show(testContainer);
waitsForChange();
});
it("populates its container", function () {
expect(testContainer.children.length > 0).toBe(true);
});
describe("when rows have been populated", function () {
function rowsMatch() {
var rows = $(testContainer).find(".l-autoflow-row").length;
return rows === testChildren.length;
}
it("shows one row per child object", function () {
waitsFor(rowsMatch);
});
it("adds rows on composition change", function () {
var child = {
identifier: { namespace: "test", key: "123" },
name: "Object 123"
};
testChildren.push(child);
emitEvent(mockComposition, 'add', child);
waitsFor(rowsMatch);
});
it("removes rows on composition change", function () {
var child = testChildren.pop();
emitEvent(mockComposition, 'remove', child.identifier);
waitsFor(rowsMatch);
});
});
it("removes subscriptions when destroyed", function () {
testKeys.forEach(function (key) {
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
});
view.destroy();
testKeys.forEach(function (key) {
expect(mockUnsubscribes[key]).toHaveBeenCalled();
});
});
it("provides a button to change column width", function () {
var initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
var nextWidth =
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
expect($(testContainer).find('.l-autoflow-col').css('width'))
.toEqual(initialWidth + 'px');
$(testContainer).find('.change-column-width').click();
waitsFor(function () {
var width = $(testContainer).find('.l-autoflow-col').css('width');
return width !== initialWidth + 'px';
});
runs(function () {
expect($(testContainer).find('.l-autoflow-col').css('width'))
.toEqual(nextWidth + 'px');
});
});
it("subscribes to all child objects", function () {
testKeys.forEach(function (key) {
expect(callbacks[key]).toEqual(jasmine.any(Function));
});
});
it("displays historical telemetry", function () {
waitsFor(function () {
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
});
runs(function () {
testKeys.forEach(function (key, index) {
var datum = testHistories[key];
var $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
});
it("displays incoming telemetry", function () {
var testData = testKeys.map(function (key, index) {
return { key: key, range: index * 100, domain: key + index };
});
testData.forEach(function (datum) {
callbacks[datum.key](datum);
});
waitsForChange();
runs(function () {
testData.forEach(function (datum, index) {
var $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
});
it("updates classes for limit violations", function () {
var testClass = "some-limit-violation";
mockEvaluator.evaluate.andReturn({ cssClass: testClass });
testKeys.forEach(function (key) {
callbacks[key]({ range: 'foo', domain: 'bar' });
});
waitsForChange();
runs(function () {
testKeys.forEach(function (datum, index) {
var $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.hasClass(testClass)).toBe(true);
});
});
});
it("automatically flows to new columns", function () {
var rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
var sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
var count = testKeys.length;
var $container = $(testContainer);
function columnsHaveAutoflowed() {
var itemsHeight = $container.find('.l-autoflow-items').height();
var availableHeight = itemsHeight - sliderHeight;
var availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
var columns = Math.ceil(count / availableRows);
return $container.find('.l-autoflow-col').length === columns;
}
$container.find('.abs').css({
position: 'absolute',
left: '0px',
right: '0px',
top: '0px',
bottom: '0px'
});
$container.css({ position: 'absolute' });
runs($container.appendTo.bind($container, document.body));
for (var height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
runs($container.css.bind($container, 'height', height + 'px'));
waitsFor(columnsHaveAutoflowed);
}
runs($container.remove.bind($container));
});
it("loads composition exactly once", function () {
var testObj = testChildren.pop();
emitEvent(mockComposition, 'remove', testObj.identifier);
testChildren.push(testObj);
emitEvent(mockComposition, 'add', testObj);
expect(mockComposition.load.calls.length).toEqual(1);
});
});
});
});
});

Some files were not shown because too many files have changed in this diff Show More