Compare commits
21 Commits
summary-wi
...
compositio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
195f48a267 | ||
|
|
d4e3e6689c | ||
|
|
0363d0e8ad | ||
|
|
3669e776a9 | ||
|
|
5d3adc6a7f | ||
|
|
c1b2db848a | ||
|
|
5d19294c11 | ||
|
|
9b8d5f3f9c | ||
|
|
d03f323a9b | ||
|
|
54a453e5a0 | ||
|
|
14894cf197 | ||
|
|
0c6786198a | ||
|
|
6d077b775d | ||
|
|
144437a06e | ||
|
|
557cd91b21 | ||
|
|
39d3e92094 | ||
|
|
7529a86d01 | ||
|
|
d34e36831c | ||
|
|
aa8fa9168a | ||
|
|
3f1b7e0a87 | ||
|
|
5ec3b98d1c |
@@ -21,5 +21,6 @@
|
|||||||
"shadow": "outer",
|
"shadow": "outer",
|
||||||
"strict": "implied",
|
"strict": "implied",
|
||||||
"undef": true,
|
"undef": true,
|
||||||
"unused": "vars"
|
"unused": "vars",
|
||||||
|
"latedef": "nofunc"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ and [`gulp`](http://gulpjs.com/).
|
|||||||
|
|
||||||
To build Open MCT for deployment:
|
To build Open MCT for deployment:
|
||||||
|
|
||||||
`npm run prepublish`
|
`npm run prepare`
|
||||||
|
|
||||||
This will compile and minify JavaScript sources, as well as copy over assets.
|
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
|
The contents of the `dist` folder will contain a runnable Open MCT
|
||||||
|
|||||||
10
circle.yml
10
circle.yml
@@ -1,3 +1,11 @@
|
|||||||
|
machine:
|
||||||
|
node:
|
||||||
|
version: 4.7.0
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
pre:
|
||||||
|
- npm install -g npm@latest
|
||||||
|
|
||||||
deployment:
|
deployment:
|
||||||
production:
|
production:
|
||||||
branch: master
|
branch: master
|
||||||
@@ -16,4 +24,4 @@ test:
|
|||||||
general:
|
general:
|
||||||
branches:
|
branches:
|
||||||
ignore:
|
ignore:
|
||||||
- gh-pages
|
- gh-pages
|
||||||
@@ -2283,7 +2283,7 @@ To install build dependencies (only needs to be run once):
|
|||||||
|
|
||||||
To build:
|
To build:
|
||||||
|
|
||||||
`npm run prepublish`
|
`npm run prepare`
|
||||||
|
|
||||||
This will compile and minify JavaScript sources, as well as copy over assets.
|
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
|
The contents of the `dist` folder will contain a runnable Open MCT
|
||||||
|
|||||||
121
docs/src/guide/security.md
Normal file
121
docs/src/guide/security.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Security Guide
|
||||||
|
|
||||||
|
Open MCT is a rich client with plugin support that executes as a single page
|
||||||
|
web application in a browser environment. Security concerns and
|
||||||
|
vulnerabilities associated with the web as a platform should be considered
|
||||||
|
before deploying Open MCT (or any other web application) for mission or
|
||||||
|
production usage.
|
||||||
|
|
||||||
|
This document describes several important points to consider when developing
|
||||||
|
for or deploying Open MCT securely. Other resources such as
|
||||||
|
[Open Web Application Security Project (OWASP)](https://www.owasp.org)
|
||||||
|
provide a deeper and more general overview of security for web applications.
|
||||||
|
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
Open MCT has been architected assuming the following deployment pattern:
|
||||||
|
|
||||||
|
* A tagged, tested Open MCT version will be used.
|
||||||
|
* Externally authored plugins will be installed.
|
||||||
|
* A server will provide persistent storage, telemetry, and other shared data.
|
||||||
|
* Authorization, authentication, and auditing will be handled by a server.
|
||||||
|
|
||||||
|
|
||||||
|
## Security Procedures
|
||||||
|
|
||||||
|
The Open MCT team secures our code base using a combination of code review,
|
||||||
|
dependency review, and periodic security reviews. Static analysis performed
|
||||||
|
during automated verification additionally safeguards against common
|
||||||
|
coding errors which may result in vulnerabilities.
|
||||||
|
|
||||||
|
|
||||||
|
### Code Review
|
||||||
|
|
||||||
|
All contributions are reviewed by internal team members. External
|
||||||
|
contributors receive increased scrutiny for security and quality,
|
||||||
|
and must sign a licensing agreement.
|
||||||
|
|
||||||
|
### Dependency Review
|
||||||
|
|
||||||
|
Before integrating third-party dependencies, they are reviewed for security
|
||||||
|
and quality, with consideration given to authors and users of these
|
||||||
|
dependencies, as well as review of open source code.
|
||||||
|
|
||||||
|
### Periodic Security Reviews
|
||||||
|
|
||||||
|
Open MCT's code, design, and architecture are periodically reviewed
|
||||||
|
(approximately annually) for common security issues, such as the
|
||||||
|
[OWASP Top Ten](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project).
|
||||||
|
|
||||||
|
|
||||||
|
## Security Concerns
|
||||||
|
|
||||||
|
Certain security concerns deserve special attention when deploying Open MCT,
|
||||||
|
or when authoring plugins.
|
||||||
|
|
||||||
|
### Identity Spoofing
|
||||||
|
|
||||||
|
Open MCT issues calls to web services with the privileges of a logged in user.
|
||||||
|
Compromised sources (either for Open MCT itself or a plugin) could
|
||||||
|
therefore allow malicious code to execute with those privileges.
|
||||||
|
|
||||||
|
To avoid this:
|
||||||
|
|
||||||
|
* Serve Open MCT and other scripts over SSL (https rather than http)
|
||||||
|
to prevent man-in-the-middle attacks.
|
||||||
|
* Exercise precautions such as security reviews for any plugins or
|
||||||
|
applications built for or with Open MCT to reject malicious changes.
|
||||||
|
|
||||||
|
### Information Disclosure
|
||||||
|
|
||||||
|
If Open MCT is used to handle or display sensitive data, any components
|
||||||
|
(such as adapter plugins) must take care to avoid leaking or disclosing
|
||||||
|
this information. For example, avoid sending sensitive data to third-party
|
||||||
|
servers or insecure APIs.
|
||||||
|
|
||||||
|
### Data Tampering
|
||||||
|
|
||||||
|
The web application architecture leaves open the possibility that direct
|
||||||
|
calls will be made to back-end services, circumventing Open MCT entirely.
|
||||||
|
As such, Open MCT assumes that server components will perform any necessary
|
||||||
|
data validation during calls issues to the server.
|
||||||
|
|
||||||
|
Additionally, plugins which serialize and write data to the server must
|
||||||
|
escape that data to avoid database injection attacks, and similar.
|
||||||
|
|
||||||
|
### Repudiation
|
||||||
|
|
||||||
|
Open MCT assumes that servers log any relevant interactions and associates
|
||||||
|
these with a user identity; the specific user actions taken within the
|
||||||
|
application are assumed not to be of concern for auditing.
|
||||||
|
|
||||||
|
In the absence of server-side logging, users may disclaim (maliciously,
|
||||||
|
mistakenly, or otherwise) actions taken within the system without any
|
||||||
|
way to prove otherwise.
|
||||||
|
|
||||||
|
If keeping client-level interactions is important, this will need to be
|
||||||
|
implemented via a plugin.
|
||||||
|
|
||||||
|
### Denial-of-service
|
||||||
|
|
||||||
|
Open MCT assumes that server-side components will be insulated against
|
||||||
|
denial-of-service attacks. Services should only permit resource-intensive
|
||||||
|
tasks to be initiated by known or trusted users.
|
||||||
|
|
||||||
|
### Elevation of Privilege
|
||||||
|
|
||||||
|
Corollary to the assumption that servers guide against identity spoofing,
|
||||||
|
Open MCT assumes that services do not allow a user to act with
|
||||||
|
inappropriately escalated privileges. Open MCT cannot protect against
|
||||||
|
such escalation; in the clearest case, a malicious actor could interact
|
||||||
|
with web services directly to exploit such a vulnerability.
|
||||||
|
|
||||||
|
## Additional Reading
|
||||||
|
|
||||||
|
The following resources have been used as a basis for identifying potential
|
||||||
|
security threats to Open MCT deployments in preparation of this document:
|
||||||
|
|
||||||
|
* [STRIDE model](https://www.owasp.org/index.php/Threat_Risk_Modeling#STRIDE)
|
||||||
|
* [Attack Surface Analysis Cheat Sheet](https://www.owasp.org/index.php/Attack_Surface_Analysis_Cheat_Sheet)
|
||||||
|
* [XSS Prevention Cheat Sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet)
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
/*global require,__dirname*/
|
/*global require,__dirname*/
|
||||||
|
|
||||||
|
require("v8-compile-cache");
|
||||||
|
|
||||||
var gulp = require('gulp'),
|
var gulp = require('gulp'),
|
||||||
sourcemaps = require('gulp-sourcemaps'),
|
sourcemaps = require('gulp-sourcemaps'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
@@ -177,4 +179,4 @@ gulp.task('install', [ 'assets', 'scripts' ]);
|
|||||||
|
|
||||||
gulp.task('verify', [ 'lint', 'test', 'checkstyle' ]);
|
gulp.task('verify', [ 'lint', 'test', 'checkstyle' ]);
|
||||||
|
|
||||||
gulp.task('build', [ 'verify', 'install' ]);
|
gulp.task('build', [ 'verify', 'install' ]);
|
||||||
@@ -40,7 +40,7 @@ requirejs.config({
|
|||||||
"vue": "node_modules/vue/dist/vue.min",
|
"vue": "node_modules/vue/dist/vue.min",
|
||||||
"zepto": "bower_components/zepto/zepto.min",
|
"zepto": "bower_components/zepto/zepto.min",
|
||||||
"lodash": "bower_components/lodash/lodash",
|
"lodash": "bower_components/lodash/lodash",
|
||||||
"d3-selection": "node_modules/d3-selection/build/d3-selection.min",
|
"d3-selection": "node_modules/d3-selection/dist/d3-selection.min",
|
||||||
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
|
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
|
||||||
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
|
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
|
||||||
"d3-array": "node_modules/d3-array/build/d3-array.min",
|
"d3-array": "node_modules/d3-array/build/d3-array.min",
|
||||||
|
|||||||
@@ -50,7 +50,8 @@
|
|||||||
"moment": "^2.11.1",
|
"moment": "^2.11.1",
|
||||||
"node-bourbon": "^4.2.3",
|
"node-bourbon": "^4.2.3",
|
||||||
"requirejs": "2.1.x",
|
"requirejs": "2.1.x",
|
||||||
"split": "^1.0.0"
|
"split": "^1.0.0",
|
||||||
|
"v8-compile-cache": "^1.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node app.js",
|
"start": "node app.js",
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
|
"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'",
|
"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",
|
"docs": "npm run jsdoc ; npm run otherdoc",
|
||||||
"prepublish": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install"
|
"prepare": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -28,16 +28,6 @@ define(
|
|||||||
[],
|
[],
|
||||||
function () {
|
function () {
|
||||||
|
|
||||||
function isDirty(domainObject) {
|
|
||||||
var navigatedObject = domainObject,
|
|
||||||
editorCapability = navigatedObject &&
|
|
||||||
navigatedObject.getCapability("editor");
|
|
||||||
|
|
||||||
return editorCapability &&
|
|
||||||
editorCapability.isEditContextRoot() &&
|
|
||||||
editorCapability.dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelEditing(domainObject) {
|
function cancelEditing(domainObject) {
|
||||||
var navigatedObject = domainObject,
|
var navigatedObject = domainObject,
|
||||||
editorCapability = navigatedObject &&
|
editorCapability = navigatedObject &&
|
||||||
@@ -59,10 +49,7 @@ define(
|
|||||||
|
|
||||||
var removeCheck = navigationService
|
var removeCheck = navigationService
|
||||||
.checkBeforeNavigation(function () {
|
.checkBeforeNavigation(function () {
|
||||||
if (isDirty(domainObject)) {
|
return "Continuing will cause the loss of any unsaved changes.";
|
||||||
return "Continuing will cause the loss of any unsaved changes.";
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.$on('$destroy', function () {
|
$scope.$on('$destroy', function () {
|
||||||
|
|||||||
@@ -52,8 +52,20 @@ define(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setSelection(selection) {
|
function setSelection(selection) {
|
||||||
self.scope.selection = selection;
|
if (!selection[0]) {
|
||||||
self.refreshComposition(selection);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.mutationListener) {
|
||||||
|
self.mutationListener();
|
||||||
|
delete self.mutationListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
var domainObject = selection[0].context.oldItem;
|
||||||
|
self.refreshComposition(domainObject);
|
||||||
|
|
||||||
|
self.mutationListener = domainObject.getCapability('mutation')
|
||||||
|
.listen(self.refreshComposition.bind(self, domainObject));
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.filterBy = filterBy;
|
$scope.filterBy = filterBy;
|
||||||
@@ -70,15 +82,11 @@ define(
|
|||||||
/**
|
/**
|
||||||
* Gets the composition for the selected object and populates the scope with it.
|
* Gets the composition for the selected object and populates the scope with it.
|
||||||
*
|
*
|
||||||
* @param selection the selection object
|
* @param domainObject the selected object
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
ElementsController.prototype.refreshComposition = function (selection) {
|
ElementsController.prototype.refreshComposition = function (domainObject) {
|
||||||
if (!selection[0]) {
|
var selectedObjectComposition = domainObject.useCapability('composition');
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedObjectComposition = selection[0].context.oldItem.useCapability('composition');
|
|
||||||
|
|
||||||
if (selectedObjectComposition) {
|
if (selectedObjectComposition) {
|
||||||
selectedObjectComposition.then(function (composition) {
|
selectedObjectComposition.then(function (composition) {
|
||||||
|
|||||||
@@ -44,11 +44,17 @@ define(
|
|||||||
this.selectedObj = undefined;
|
this.selectedObj = undefined;
|
||||||
|
|
||||||
openmct.selection.on('change', function (selection) {
|
openmct.selection.on('change', function (selection) {
|
||||||
if (selection[0] && selection[0].context.toolbar) {
|
var selected = selection[0];
|
||||||
this.select(selection[0].context.toolbar);
|
|
||||||
|
if (selected && selected.context.toolbar) {
|
||||||
|
this.select(selected.context.toolbar);
|
||||||
} else {
|
} else {
|
||||||
this.deselect();
|
this.deselect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selected && selected.context.viewProxy) {
|
||||||
|
this.proxy(selected.context.viewProxy);
|
||||||
|
}
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,10 +104,10 @@ define(
|
|||||||
mockEditorCapability.isEditContextRoot.andReturn(false);
|
mockEditorCapability.isEditContextRoot.andReturn(false);
|
||||||
mockEditorCapability.dirty.andReturn(false);
|
mockEditorCapability.dirty.andReturn(false);
|
||||||
|
|
||||||
expect(checkFn()).toBe(false);
|
expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes.");
|
||||||
|
|
||||||
mockEditorCapability.isEditContextRoot.andReturn(true);
|
mockEditorCapability.isEditContextRoot.andReturn(true);
|
||||||
expect(checkFn()).toBe(false);
|
expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes.");
|
||||||
|
|
||||||
mockEditorCapability.dirty.andReturn(true);
|
mockEditorCapability.dirty.andReturn(true);
|
||||||
expect(checkFn())
|
expect(checkFn())
|
||||||
|
|||||||
@@ -29,9 +29,25 @@ define(
|
|||||||
var mockScope,
|
var mockScope,
|
||||||
mockOpenMCT,
|
mockOpenMCT,
|
||||||
mockSelection,
|
mockSelection,
|
||||||
|
mockDomainObject,
|
||||||
|
mockMutationCapability,
|
||||||
|
mockUnlisten,
|
||||||
|
selectable = [],
|
||||||
controller;
|
controller;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
mockUnlisten = jasmine.createSpy('unlisten');
|
||||||
|
mockMutationCapability = jasmine.createSpyObj("mutationCapability", [
|
||||||
|
"listen"
|
||||||
|
]);
|
||||||
|
mockMutationCapability.listen.andReturn(mockUnlisten);
|
||||||
|
mockDomainObject = jasmine.createSpyObj("domainObject", [
|
||||||
|
"getCapability",
|
||||||
|
"useCapability"
|
||||||
|
]);
|
||||||
|
mockDomainObject.useCapability.andCallThrough();
|
||||||
|
mockDomainObject.getCapability.andReturn(mockMutationCapability);
|
||||||
|
|
||||||
mockScope = jasmine.createSpyObj("$scope", ['$on']);
|
mockScope = jasmine.createSpyObj("$scope", ['$on']);
|
||||||
mockSelection = jasmine.createSpyObj("selection", [
|
mockSelection = jasmine.createSpyObj("selection", [
|
||||||
'on',
|
'on',
|
||||||
@@ -43,6 +59,14 @@ define(
|
|||||||
selection: mockSelection
|
selection: mockSelection
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectable[0] = {
|
||||||
|
context: {
|
||||||
|
oldItem: mockDomainObject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
spyOn(ElementsController.prototype, 'refreshComposition');
|
||||||
|
|
||||||
controller = new ElementsController(mockScope, mockOpenMCT);
|
controller = new ElementsController(mockScope, mockOpenMCT);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,6 +99,34 @@ define(
|
|||||||
expect(objects.filter(mockScope.searchElements).length).toBe(4);
|
expect(objects.filter(mockScope.searchElements).length).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("refreshes composition on selection", function () {
|
||||||
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
|
|
||||||
|
expect(ElementsController.prototype.refreshComposition).toHaveBeenCalledWith(mockDomainObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("listens on mutation and refreshes composition", function () {
|
||||||
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
|
|
||||||
|
expect(mockDomainObject.getCapability).toHaveBeenCalledWith('mutation');
|
||||||
|
expect(mockMutationCapability.listen).toHaveBeenCalled();
|
||||||
|
expect(ElementsController.prototype.refreshComposition.calls.length).toBe(1);
|
||||||
|
|
||||||
|
mockMutationCapability.listen.mostRecentCall.args[0](mockDomainObject);
|
||||||
|
|
||||||
|
expect(ElementsController.prototype.refreshComposition.calls.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cleans up mutation listener when selection changes", function () {
|
||||||
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
|
|
||||||
|
expect(mockMutationCapability.listen).toHaveBeenCalled();
|
||||||
|
|
||||||
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
|
|
||||||
|
expect(mockUnlisten).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
$ohH: $btnFrameH;
|
$ohH: $btnFrameH;
|
||||||
$bc: $colorInteriorBorder;
|
$bc: $colorInteriorBorder;
|
||||||
&.child-frame.panel {
|
&.child-frame.panel {
|
||||||
border: 1px solid transparent;
|
|
||||||
z-index: 0; // Needed to prevent child-frame controls from showing through when another child-frame is above
|
z-index: 0; // Needed to prevent child-frame controls from showing through when another child-frame is above
|
||||||
&:not(.no-frame) {
|
&:not(.no-frame) {
|
||||||
background: $colorBodyBg;
|
background: $colorBodyBg;
|
||||||
@@ -91,7 +90,7 @@
|
|||||||
|
|
||||||
&.no-frame {
|
&.no-frame {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: none;
|
border-color: transparent;
|
||||||
.object-browse-bar .right {
|
.object-browse-bar .right {
|
||||||
$m: 0;
|
$m: 0;
|
||||||
background: rgba(black, 0.3);
|
background: rgba(black, 0.3);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
.s-hover-border {
|
.s-hover-border {
|
||||||
|
border: 1px solid transparent;
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: rgba($colorSelectableSelectedPrimary, 0.5) !important;
|
border-color: rgba($colorSelectableSelectedPrimary, 0.5) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ define(
|
|||||||
options = Object.create(OPTIONS);
|
options = Object.create(OPTIONS);
|
||||||
options.marginX = -bubbleSpaceLR;
|
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,
|
// On a phone, bubble takes up more screen real estate,
|
||||||
// so position it differently (toward the bottom)
|
// so position it differently (toward the bottom)
|
||||||
if (this.agentService.isPhone()) {
|
if (this.agentService.isPhone()) {
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ define(
|
|||||||
view.show(container);
|
view.show(container);
|
||||||
} else {
|
} else {
|
||||||
self.providerView = false;
|
self.providerView = false;
|
||||||
$scope.inspectorKey = selection[0].context.oldItem.getCapability("type").typeDef.inspector;
|
var selectedItem = selection[0].context.oldItem;
|
||||||
|
|
||||||
|
if (selectedItem) {
|
||||||
|
$scope.inspectorKey = selectedItem.getCapability("type").typeDef.inspector;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -272,7 +272,8 @@ define([
|
|||||||
"$scope",
|
"$scope",
|
||||||
"$q",
|
"$q",
|
||||||
"dialogService",
|
"dialogService",
|
||||||
"openmct"
|
"openmct",
|
||||||
|
"$element"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
ng-controller="FixedController as controller">
|
ng-controller="FixedController as controller">
|
||||||
|
|
||||||
<!-- Background grid -->
|
<!-- Background grid -->
|
||||||
<div class="l-grid-holder" ng-click="controller.clearSelection()">
|
<div class="l-grid-holder" ng-click="controller.bypassSelection($event)">
|
||||||
<div class="l-grid l-grid-x"
|
<div class="l-grid l-grid-x"
|
||||||
ng-if="!controller.getGridSize()[0] < 3"
|
ng-if="!controller.getGridSize()[0] < 3"
|
||||||
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
|
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
|
||||||
@@ -35,35 +35,28 @@
|
|||||||
<!-- Fixed position elements -->
|
<!-- Fixed position elements -->
|
||||||
<div ng-repeat="element in controller.getElements()"
|
<div ng-repeat="element in controller.getElements()"
|
||||||
class="l-fixed-position-item s-selectable s-moveable s-hover-border"
|
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"
|
ng-style="element.style"
|
||||||
ng-click="controller.select(element, $event)">
|
mct-selectable="controller.getContext(element)"
|
||||||
|
mct-init-select="controller.shouldSelect(element)">
|
||||||
<mct-include key="element.template"
|
<mct-include key="element.template"
|
||||||
parameters="{ gridSize: controller.getGridSize() }"
|
parameters="{ gridSize: controller.getGridSize() }"
|
||||||
ng-model="element">
|
ng-model="element">
|
||||||
</mct-include>
|
</mct-include>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selection highlight, handles -->
|
<!-- Selection highlight, handles -->
|
||||||
<span class="s-selected s-moveable" ng-if="controller.selected()">
|
<span class="s-selected s-moveable" ng-if="controller.isElementSelected()">
|
||||||
<div class="l-fixed-position-item t-edit-handle-holder"
|
<div class="l-fixed-position-item t-edit-handle-holder"
|
||||||
mct-drag-down="controller.moveHandle().startDrag(controller.selected())"
|
mct-drag-down="controller.moveHandle().startDrag()"
|
||||||
mct-drag="controller.moveHandle().continueDrag(delta)"
|
mct-drag="controller.moveHandle().continueDrag(delta)"
|
||||||
mct-drag-up="controller.moveHandle().endDrag()"
|
mct-drag-up="controller.endDrag()"
|
||||||
ng-style="controller.selected().style"
|
ng-style="controller.getSelectedElementStyle()">
|
||||||
ng-click="$event.stopPropagation()">
|
|
||||||
</div>
|
</div>
|
||||||
<div ng-repeat="handle in controller.handles()"
|
<div ng-repeat="handle in controller.handles()"
|
||||||
class="l-fixed-position-item-handle edit-corner"
|
class="l-fixed-position-item-handle edit-corner"
|
||||||
ng-style="handle.style()"
|
ng-style="handle.style()"
|
||||||
mct-drag-down="handle.startDrag()"
|
mct-drag-down="handle.startDrag()"
|
||||||
mct-drag="handle.continueDrag(delta)"
|
mct-drag="handle.continueDrag(delta)"
|
||||||
mct-drag-up="handle.endDrag()"
|
mct-drag-up="controller.endDrag(handle)">
|
||||||
ng-click="$event.stopPropagation()">
|
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ define(
|
|||||||
* @constructor
|
* @constructor
|
||||||
* @param {Scope} $scope the controller's Angular scope
|
* @param {Scope} $scope the controller's Angular scope
|
||||||
*/
|
*/
|
||||||
function FixedController($scope, $q, dialogService, openmct) {
|
function FixedController($scope, $q, dialogService, openmct, $element) {
|
||||||
this.names = {}; // Cache names by ID
|
this.names = {}; // Cache names by ID
|
||||||
this.values = {}; // Cache values by ID
|
this.values = {}; // Cache values by ID
|
||||||
this.elementProxiesById = {};
|
this.elementProxiesById = {};
|
||||||
@@ -55,9 +55,11 @@ define(
|
|||||||
this.telemetryObjects = [];
|
this.telemetryObjects = [];
|
||||||
this.subscriptions = [];
|
this.subscriptions = [];
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
|
this.$element = $element;
|
||||||
this.$scope = $scope;
|
this.$scope = $scope;
|
||||||
|
|
||||||
this.gridSize = $scope.domainObject && $scope.domainObject.getModel().layoutGrid;
|
this.gridSize = $scope.domainObject && $scope.domainObject.getModel().layoutGrid;
|
||||||
|
this.fixedViewSelectable = false;
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
[
|
[
|
||||||
@@ -87,9 +89,8 @@ define(
|
|||||||
|
|
||||||
// Update the style for a selected element
|
// Update the style for a selected element
|
||||||
function updateSelectionStyle() {
|
function updateSelectionStyle() {
|
||||||
var element = self.selection && self.selection.get();
|
if (self.selectedElementProxy) {
|
||||||
if (element) {
|
self.selectedElementProxy.style = convertPosition(self.selectedElementProxy);
|
||||||
element.style = convertPosition(element);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,25 +137,19 @@ define(
|
|||||||
|
|
||||||
// Decorate elements in the current configuration
|
// Decorate elements in the current configuration
|
||||||
function refreshElements() {
|
function refreshElements() {
|
||||||
// Cache selection; we are instantiating new proxies
|
var elements = (($scope.configuration || {}).elements || []);
|
||||||
// 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...
|
// Create the new proxies...
|
||||||
self.elementProxies = elements.map(makeProxyElement);
|
self.elementProxies = elements.map(makeProxyElement);
|
||||||
|
|
||||||
// Clear old selection, and restore if appropriate
|
// If selection is not in array, select parent.
|
||||||
if (self.selection) {
|
// Otherwise, set the element to select after refresh.
|
||||||
self.selection.deselect();
|
if (self.selectedElementProxy) {
|
||||||
if (index > -1) {
|
var index = elements.indexOf(self.selectedElementProxy.element);
|
||||||
self.select(self.elementProxies[index]);
|
if (index === -1) {
|
||||||
|
self.$element[0].click();
|
||||||
|
} else if (!self.elementToSelectAfterRefresh) {
|
||||||
|
self.elementToSelectAfterRefresh = self.elementProxies[index].element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,12 +219,12 @@ define(
|
|||||||
$scope.configuration.elements || [];
|
$scope.configuration.elements || [];
|
||||||
// Store the position of this element.
|
// Store the position of this element.
|
||||||
$scope.configuration.elements.push(element);
|
$scope.configuration.elements.push(element);
|
||||||
|
|
||||||
|
self.elementToSelectAfterRefresh = element;
|
||||||
|
|
||||||
// Refresh displayed elements
|
// Refresh displayed elements
|
||||||
refreshElements();
|
refreshElements();
|
||||||
// Select the newly-added element
|
|
||||||
self.select(
|
|
||||||
self.elementProxies[self.elementProxies.length - 1]
|
|
||||||
);
|
|
||||||
// Mark change as persistable
|
// Mark change as persistable
|
||||||
if ($scope.commit) {
|
if ($scope.commit) {
|
||||||
$scope.commit("Dropped an element.");
|
$scope.commit("Dropped an element.");
|
||||||
@@ -263,21 +258,36 @@ define(
|
|||||||
self.getTelemetry($scope.domainObject);
|
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.elementProxies = [];
|
||||||
this.generateDragHandle = generateDragHandle;
|
this.generateDragHandle = generateDragHandle;
|
||||||
this.generateDragHandles = generateDragHandles;
|
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
|
// Detect changes to grid size
|
||||||
$scope.$watch("model.layoutGrid", updateElementPositions);
|
$scope.$watch("model.layoutGrid", updateElementPositions);
|
||||||
@@ -298,10 +308,13 @@ define(
|
|||||||
$scope.$on("$destroy", function () {
|
$scope.$on("$destroy", function () {
|
||||||
self.unsubscribe();
|
self.unsubscribe();
|
||||||
self.openmct.time.off("bounds", updateDisplayBounds);
|
self.openmct.time.off("bounds", updateDisplayBounds);
|
||||||
|
self.openmct.selection.off("change", setSelection);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Respond to external bounds changes
|
// Respond to external bounds changes
|
||||||
this.openmct.time.on("bounds", updateDisplayBounds);
|
this.openmct.time.on("bounds", updateDisplayBounds);
|
||||||
|
this.openmct.selection.on('change', setSelection);
|
||||||
|
this.$element.on('click', this.bypassSelection.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -492,42 +505,56 @@ define(
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the element is currently selected, or (if no
|
* Checks if the element should be selected or not.
|
||||||
* argument is supplied) get the currently selected element.
|
*
|
||||||
* @returns {boolean} true if selected
|
* @param elementProxy the element to check
|
||||||
|
* @returns {boolean} true if the element should be selected.
|
||||||
*/
|
*/
|
||||||
FixedController.prototype.selected = function (element) {
|
FixedController.prototype.shouldSelect = function (elementProxy) {
|
||||||
var selection = this.selection;
|
if (elementProxy.element === this.elementToSelectAfterRefresh) {
|
||||||
return selection && ((arguments.length > 0) ?
|
delete this.elementToSelectAfterRefresh;
|
||||||
selection.selected(element) : selection.get());
|
return true;
|
||||||
};
|
} else {
|
||||||
|
return false;
|
||||||
/**
|
|
||||||
* Set the active user selection in this view.
|
|
||||||
* @param element the element to select
|
|
||||||
*/
|
|
||||||
FixedController.prototype.select = function select(element, event) {
|
|
||||||
if (event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the current user selection.
|
* Checks if an element is currently selected.
|
||||||
|
*
|
||||||
|
* @returns {boolean} true if an element is selected.
|
||||||
*/
|
*/
|
||||||
FixedController.prototype.clearSelection = function () {
|
FixedController.prototype.isElementSelected = function () {
|
||||||
if (this.selection) {
|
return (this.selectedElementProxy) ? true : false;
|
||||||
this.selection.deselect();
|
};
|
||||||
this.resizeHandles = [];
|
|
||||||
this.mvHandle = undefined;
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -548,6 +575,38 @@ define(
|
|||||||
return this.mvHandle;
|
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;
|
return FixedController;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ define(
|
|||||||
* Start a drag gesture. This should be called when a drag
|
* Start a drag gesture. This should be called when a drag
|
||||||
* begins to track initial state.
|
* begins to track initial state.
|
||||||
*/
|
*/
|
||||||
FixedDragHandle.prototype.startDrag = function startDrag() {
|
FixedDragHandle.prototype.startDrag = function () {
|
||||||
// Cache initial x/y positions
|
// Cache initial x/y positions
|
||||||
this.dragging = {
|
this.dragging = {
|
||||||
x: this.elementHandle.x(),
|
x: this.elementHandle.x(),
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ define(
|
|||||||
* @param element the fixed position element, as stored in its
|
* @param element the fixed position element, as stored in its
|
||||||
* configuration
|
* configuration
|
||||||
* @param index the element's index within its array
|
* @param index the element's index within its array
|
||||||
* @param {number[]} gridSize the current layout grid size in [x,y] from
|
|
||||||
* @param {Array} elements the full array of elements
|
* @param {Array} elements the full array of elements
|
||||||
|
* @param {number[]} gridSize the current layout grid size in [x,y] from
|
||||||
*/
|
*/
|
||||||
function ElementProxy(element, index, elements, gridSize) {
|
function ElementProxy(element, index, elements, gridSize) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,8 +21,14 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(
|
define(
|
||||||
["../src/FixedController"],
|
[
|
||||||
function (FixedController) {
|
"../src/FixedController",
|
||||||
|
"zepto"
|
||||||
|
],
|
||||||
|
function (
|
||||||
|
FixedController,
|
||||||
|
$
|
||||||
|
) {
|
||||||
|
|
||||||
describe("The Fixed Position controller", function () {
|
describe("The Fixed Position controller", function () {
|
||||||
var mockScope,
|
var mockScope,
|
||||||
@@ -46,6 +52,9 @@ define(
|
|||||||
mockMetadata,
|
mockMetadata,
|
||||||
mockTimeSystem,
|
mockTimeSystem,
|
||||||
mockLimitEvaluator,
|
mockLimitEvaluator,
|
||||||
|
mockSelection,
|
||||||
|
$element = [],
|
||||||
|
selectable = [],
|
||||||
controller;
|
controller;
|
||||||
|
|
||||||
// Utility function; find a watch for a given expression
|
// Utility function; find a watch for a given expression
|
||||||
@@ -180,17 +189,30 @@ define(
|
|||||||
|
|
||||||
mockScope.model = testModel;
|
mockScope.model = testModel;
|
||||||
mockScope.configuration = testConfiguration;
|
mockScope.configuration = testConfiguration;
|
||||||
mockScope.selection = jasmine.createSpyObj(
|
|
||||||
'selection',
|
selectable[0] = {
|
||||||
['select', 'get', 'selected', 'deselect', 'proxy']
|
context: {
|
||||||
);
|
oldItem: mockDomainObject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockSelection = jasmine.createSpyObj("selection", [
|
||||||
|
'select',
|
||||||
|
'on',
|
||||||
|
'off',
|
||||||
|
'get'
|
||||||
|
]);
|
||||||
|
mockSelection.get.andCallThrough();
|
||||||
|
|
||||||
mockOpenMCT = {
|
mockOpenMCT = {
|
||||||
time: mockConductor,
|
time: mockConductor,
|
||||||
telemetry: mockTelemetryAPI,
|
telemetry: mockTelemetryAPI,
|
||||||
composition: mockCompositionAPI
|
composition: mockCompositionAPI,
|
||||||
|
selection: mockSelection
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$element = $('<div></div>');
|
||||||
|
spyOn($element[0], 'click');
|
||||||
|
|
||||||
mockMetadata = jasmine.createSpyObj('mockMetadata', [
|
mockMetadata = jasmine.createSpyObj('mockMetadata', [
|
||||||
'valuesForHints',
|
'valuesForHints',
|
||||||
'value',
|
'value',
|
||||||
@@ -226,11 +248,11 @@ define(
|
|||||||
mockScope,
|
mockScope,
|
||||||
mockQ,
|
mockQ,
|
||||||
mockDialogService,
|
mockDialogService,
|
||||||
mockOpenMCT
|
mockOpenMCT,
|
||||||
|
$element
|
||||||
);
|
);
|
||||||
|
|
||||||
findWatch("model.layoutGrid")(testModel.layoutGrid);
|
findWatch("model.layoutGrid")(testModel.layoutGrid);
|
||||||
findWatch("selection")(mockScope.selection);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("subscribes when a domain object is available", function () {
|
it("subscribes when a domain object is available", function () {
|
||||||
@@ -306,41 +328,41 @@ define(
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("allows elements to be selected", function () {
|
it("allows elements to be selected", function () {
|
||||||
var elements;
|
|
||||||
|
|
||||||
testModel.modified = 1;
|
testModel.modified = 1;
|
||||||
findWatch("model.modified")(testModel.modified);
|
findWatch("model.modified")(testModel.modified);
|
||||||
|
|
||||||
elements = controller.getElements();
|
selectable[0].context.elementProxy = controller.getElements()[1];
|
||||||
controller.select(elements[1]);
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
expect(mockScope.selection.select)
|
|
||||||
.toHaveBeenCalledWith(elements[1]);
|
expect(controller.isElementSelected()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows selection retrieval", function () {
|
it("allows selection retrieval", function () {
|
||||||
// selected with no arguments should give the current
|
|
||||||
// selection
|
|
||||||
var elements;
|
var elements;
|
||||||
|
|
||||||
testModel.modified = 1;
|
testModel.modified = 1;
|
||||||
findWatch("model.modified")(testModel.modified);
|
findWatch("model.modified")(testModel.modified);
|
||||||
|
|
||||||
elements = controller.getElements();
|
elements = controller.getElements();
|
||||||
controller.select(elements[1]);
|
selectable[0].context.elementProxy = elements[1];
|
||||||
mockScope.selection.get.andReturn(elements[1]);
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
expect(controller.selected()).toEqual(elements[1]);
|
|
||||||
|
expect(controller.getSelectedElement()).toEqual(elements[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows selections to be cleared", function () {
|
it("selects the parent view when selected element is removed", function () {
|
||||||
var elements;
|
|
||||||
|
|
||||||
testModel.modified = 1;
|
testModel.modified = 1;
|
||||||
findWatch("model.modified")(testModel.modified);
|
findWatch("model.modified")(testModel.modified);
|
||||||
|
|
||||||
elements = controller.getElements();
|
var elements = controller.getElements();
|
||||||
controller.select(elements[1]);
|
selectable[0].context.elementProxy = elements[1];
|
||||||
controller.clearSelection();
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
expect(controller.selected(elements[1])).toBeFalsy();
|
|
||||||
|
elements[1].remove();
|
||||||
|
testModel.modified = 2;
|
||||||
|
findWatch("model.modified")(testModel.modified);
|
||||||
|
|
||||||
|
expect($element[0].click).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("retains selections during refresh", function () {
|
it("retains selections during refresh", function () {
|
||||||
@@ -352,23 +374,21 @@ define(
|
|||||||
findWatch("model.modified")(testModel.modified);
|
findWatch("model.modified")(testModel.modified);
|
||||||
|
|
||||||
elements = controller.getElements();
|
elements = controller.getElements();
|
||||||
controller.select(elements[1]);
|
selectable[0].context.elementProxy = elements[1];
|
||||||
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
|
|
||||||
// Verify precondition
|
expect(controller.getSelectedElement()).toEqual(elements[1]);
|
||||||
expect(mockScope.selection.select.calls.length).toEqual(1);
|
|
||||||
|
|
||||||
// Mimic selection behavior
|
|
||||||
mockScope.selection.get.andReturn(elements[1]);
|
|
||||||
|
|
||||||
elements[2].remove();
|
elements[2].remove();
|
||||||
testModel.modified = 2;
|
testModel.modified = 2;
|
||||||
findWatch("model.modified")(testModel.modified);
|
findWatch("model.modified")(testModel.modified);
|
||||||
|
|
||||||
elements = controller.getElements();
|
elements = controller.getElements();
|
||||||
|
|
||||||
// Verify removal, as test assumes this
|
// Verify removal, as test assumes this
|
||||||
expect(elements.length).toEqual(2);
|
expect(elements.length).toEqual(2);
|
||||||
|
|
||||||
expect(mockScope.selection.select.calls.length).toEqual(2);
|
expect(controller.shouldSelect(elements[1])).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Displays received values for telemetry elements", function () {
|
it("Displays received values for telemetry elements", function () {
|
||||||
@@ -505,21 +525,25 @@ define(
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("exposes a view-level selection proxy", function () {
|
it("exposes a view-level selection proxy", function () {
|
||||||
expect(mockScope.selection.proxy).toHaveBeenCalledWith(
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
jasmine.any(Object)
|
var selection = mockOpenMCT.selection.select.mostRecentCall.args[0];
|
||||||
);
|
|
||||||
|
expect(mockOpenMCT.selection.select).toHaveBeenCalled();
|
||||||
|
expect(selection.context.viewProxy).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes drag handles", function () {
|
it("exposes drag handles", function () {
|
||||||
var handles;
|
var handles;
|
||||||
|
|
||||||
// Select something so that drag handles are expected
|
|
||||||
testModel.modified = 1;
|
testModel.modified = 1;
|
||||||
findWatch("model.modified")(testModel.modified);
|
findWatch("model.modified")(testModel.modified);
|
||||||
controller.select(controller.getElements()[1]);
|
|
||||||
|
selectable[0].context.elementProxy = controller.getElements()[1];
|
||||||
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
|
|
||||||
// Should have a non-empty array of handles
|
// Should have a non-empty array of handles
|
||||||
handles = controller.handles();
|
handles = controller.handles();
|
||||||
|
|
||||||
expect(handles).toEqual(jasmine.any(Array));
|
expect(handles).toEqual(jasmine.any(Array));
|
||||||
expect(handles.length).not.toEqual(0);
|
expect(handles.length).not.toEqual(0);
|
||||||
|
|
||||||
@@ -532,15 +556,14 @@ define(
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("exposes a move handle", function () {
|
it("exposes a move handle", function () {
|
||||||
var handle;
|
|
||||||
|
|
||||||
// Select something so that drag handles are expected
|
|
||||||
testModel.modified = 1;
|
testModel.modified = 1;
|
||||||
findWatch("model.modified")(testModel.modified);
|
findWatch("model.modified")(testModel.modified);
|
||||||
controller.select(controller.getElements()[1]);
|
|
||||||
|
selectable[0].context.elementProxy = controller.getElements()[1];
|
||||||
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
|
|
||||||
// Should have a move handle
|
// Should have a move handle
|
||||||
handle = controller.moveHandle();
|
var handle = controller.moveHandle();
|
||||||
|
|
||||||
// And it should have start/continue/end drag methods
|
// And it should have start/continue/end drag methods
|
||||||
expect(handle.startDrag).toEqual(jasmine.any(Function));
|
expect(handle.startDrag).toEqual(jasmine.any(Function));
|
||||||
@@ -551,26 +574,40 @@ define(
|
|||||||
it("updates selection style during drag", function () {
|
it("updates selection style during drag", function () {
|
||||||
var oldStyle;
|
var oldStyle;
|
||||||
|
|
||||||
// Select something so that drag handles are expected
|
|
||||||
testModel.modified = 1;
|
testModel.modified = 1;
|
||||||
findWatch("model.modified")(testModel.modified);
|
findWatch("model.modified")(testModel.modified);
|
||||||
controller.select(controller.getElements()[1]);
|
|
||||||
mockScope.selection.get.andReturn(controller.getElements()[1]);
|
selectable[0].context.elementProxy = controller.getElements()[1];
|
||||||
|
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||||
|
|
||||||
// Get style
|
// Get style
|
||||||
oldStyle = controller.selected().style;
|
oldStyle = controller.getSelectedElementStyle();
|
||||||
|
|
||||||
// Start a drag gesture
|
// Start a drag gesture
|
||||||
controller.moveHandle().startDrag();
|
controller.moveHandle().startDrag();
|
||||||
|
|
||||||
// Haven't moved yet; style shouldn't have updated yet
|
// Haven't moved yet; style shouldn't have updated yet
|
||||||
expect(controller.selected().style).toEqual(oldStyle);
|
expect(controller.getSelectedElementStyle()).toEqual(oldStyle);
|
||||||
|
|
||||||
// Drag a little
|
// Drag a little
|
||||||
controller.moveHandle().continueDrag([1000, 100]);
|
controller.moveHandle().continueDrag([1000, 100]);
|
||||||
|
|
||||||
// Style should have been updated
|
// Style should have been updated
|
||||||
expect(controller.selected().style).not.toEqual(oldStyle);
|
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)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("on display bounds changes", function () {
|
describe("on display bounds changes", function () {
|
||||||
@@ -702,6 +739,14 @@ define(
|
|||||||
expect(controller.getElements()[0].cssClass).toEqual("alarm-a");
|
expect(controller.getElements()[0].cssClass).toEqual("alarm-a");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("listens for selection change events", function () {
|
||||||
|
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
|
||||||
|
'change',
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ define(
|
|||||||
PlotController.prototype.exportPNG = function () {
|
PlotController.prototype.exportPNG = function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
self.hideExportButtons = true;
|
self.hideExportButtons = true;
|
||||||
self.exportImageService.exportPNG(self.$element[0], "plot.png").finally(function () {
|
self.exportImageService.exportPNG(self.$element[0], "plot.png", 'white').finally(function () {
|
||||||
self.hideExportButtons = false;
|
self.hideExportButtons = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -426,7 +426,7 @@ define(
|
|||||||
PlotController.prototype.exportJPG = function () {
|
PlotController.prototype.exportJPG = function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
self.hideExportButtons = true;
|
self.hideExportButtons = true;
|
||||||
self.exportImageService.exportJPG(self.$element[0], "plot.jpg").finally(function () {
|
self.exportImageService.exportJPG(self.$element[0], "plot.jpg", 'white').finally(function () {
|
||||||
self.hideExportButtons = false;
|
self.hideExportButtons = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ define(
|
|||||||
* @param {constant} EXPORT_IMAGE_TIMEOUT time in milliseconds before a timeout error is returned
|
* @param {constant} EXPORT_IMAGE_TIMEOUT time in milliseconds before a timeout error is returned
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function ExportImageService($q, $timeout, $log, EXPORT_IMAGE_TIMEOUT, injHtml2Canvas, injSaveAs, injFileReader) {
|
function ExportImageService($q, $timeout, $log, EXPORT_IMAGE_TIMEOUT, injHtml2Canvas, injSaveAs, injFileReader, injChangeBackgroundColor) {
|
||||||
self.$q = $q;
|
self.$q = $q;
|
||||||
self.$timeout = $timeout;
|
self.$timeout = $timeout;
|
||||||
self.$log = $log;
|
self.$log = $log;
|
||||||
@@ -51,6 +51,7 @@ define(
|
|||||||
self.html2canvas = injHtml2Canvas || html2canvas;
|
self.html2canvas = injHtml2Canvas || html2canvas;
|
||||||
self.saveAs = injSaveAs || saveAs;
|
self.saveAs = injSaveAs || saveAs;
|
||||||
self.reader = injFileReader || new FileReader();
|
self.reader = injFileReader || new FileReader();
|
||||||
|
self.changeBackgroundColor = injChangeBackgroundColor || self.changeBackgroundColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,16 +61,25 @@ define(
|
|||||||
* @param {string} type of image to convert the element to
|
* @param {string} type of image to convert the element to
|
||||||
* @returns {promise}
|
* @returns {promise}
|
||||||
*/
|
*/
|
||||||
function renderElement(element, type) {
|
function renderElement(element, type, color) {
|
||||||
var defer = self.$q.defer(),
|
var defer = self.$q.defer(),
|
||||||
validTypes = ["png", "jpg", "jpeg"],
|
validTypes = ["png", "jpg", "jpeg"],
|
||||||
renderTimeout;
|
renderTimeout,
|
||||||
|
originalColor;
|
||||||
|
|
||||||
if (validTypes.indexOf(type) === -1) {
|
if (validTypes.indexOf(type) === -1) {
|
||||||
self.$log.error("Invalid type requested. Try: (" + validTypes.join(",") + ")");
|
self.$log.error("Invalid type requested. Try: (" + validTypes.join(",") + ")");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
// Save color to be restored later
|
||||||
|
originalColor = element.style.backgroundColor || '';
|
||||||
|
|
||||||
|
// Defaulting to white so we can see the chart when printed
|
||||||
|
self.changeBackgroundColor(element, color);
|
||||||
|
}
|
||||||
|
|
||||||
renderTimeout = self.$timeout(function () {
|
renderTimeout = self.$timeout(function () {
|
||||||
defer.reject("html2canvas timed out");
|
defer.reject("html2canvas timed out");
|
||||||
self.$log.warn("html2canvas timed out");
|
self.$log.warn("html2canvas timed out");
|
||||||
@@ -78,13 +88,15 @@ define(
|
|||||||
try {
|
try {
|
||||||
self.html2canvas(element, {
|
self.html2canvas(element, {
|
||||||
onrendered: function (canvas) {
|
onrendered: function (canvas) {
|
||||||
|
if (color) {
|
||||||
|
self.changeBackgroundColor(element, originalColor);
|
||||||
|
}
|
||||||
|
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case "png":
|
case "png":
|
||||||
canvas.toBlob(defer.resolve, "image/png");
|
canvas.toBlob(defer.resolve, "image/png");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
|
||||||
case "jpg":
|
|
||||||
case "jpeg":
|
case "jpeg":
|
||||||
canvas.toBlob(defer.resolve, "image/jpeg");
|
canvas.toBlob(defer.resolve, "image/jpeg");
|
||||||
break;
|
break;
|
||||||
@@ -96,7 +108,13 @@ define(
|
|||||||
self.$log.warn("html2canvas failed with error: " + e);
|
self.$log.warn("html2canvas failed with error: " + e);
|
||||||
}
|
}
|
||||||
|
|
||||||
defer.promise.finally(renderTimeout.cancel);
|
defer.promise.finally(function () {
|
||||||
|
renderTimeout.cancel();
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
self.changeBackgroundColor(element, originalColor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return defer.promise;
|
return defer.promise;
|
||||||
}
|
}
|
||||||
@@ -125,14 +143,21 @@ define(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
self.changeBackgroundColor = function (element, color) {
|
||||||
|
element.style.backgroundColor = color;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a screenshot of a DOM node and exports to JPG.
|
* Takes a screenshot of a DOM node and exports to JPG.
|
||||||
* @param {node} element to be exported
|
* @param {node} element to be exported
|
||||||
* @param {string} filename the exported image
|
* @param {string} filename the exported image
|
||||||
* @returns {promise}
|
* @returns {promise}
|
||||||
*/
|
*/
|
||||||
ExportImageService.prototype.exportJPG = function (element, filename) {
|
ExportImageService.prototype.exportJPG = function (element, filename, color) {
|
||||||
return renderElement(element, "jpeg").then(function (img) {
|
return renderElement(element, "jpeg", color).then(function (img) {
|
||||||
self.saveAs(img, filename);
|
self.saveAs(img, filename);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -143,8 +168,8 @@ define(
|
|||||||
* @param {string} filename the exported image
|
* @param {string} filename the exported image
|
||||||
* @returns {promise}
|
* @returns {promise}
|
||||||
*/
|
*/
|
||||||
ExportImageService.prototype.exportPNG = function (element, filename) {
|
ExportImageService.prototype.exportPNG = function (element, filename, color) {
|
||||||
return renderElement(element, "png").then(function (img) {
|
return renderElement(element, "png", color).then(function (img) {
|
||||||
self.saveAs(img, filename);
|
self.saveAs(img, filename);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ define(
|
|||||||
mockFileReader,
|
mockFileReader,
|
||||||
mockExportTimeoutConstant,
|
mockExportTimeoutConstant,
|
||||||
testElement,
|
testElement,
|
||||||
exportImageService;
|
exportImageService,
|
||||||
|
mockChangeBackgroundColor;
|
||||||
|
|
||||||
describe("ExportImageService", function () {
|
describe("ExportImageService", function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
@@ -83,7 +84,9 @@ define(
|
|||||||
["readAsDataURL", "onloadend"]
|
["readAsDataURL", "onloadend"]
|
||||||
);
|
);
|
||||||
mockExportTimeoutConstant = 0;
|
mockExportTimeoutConstant = 0;
|
||||||
testElement = {};
|
testElement = {style: {backgroundColor: 'black'}};
|
||||||
|
|
||||||
|
mockChangeBackgroundColor = jasmine.createSpy('changeBackgroundColor');
|
||||||
|
|
||||||
exportImageService = new ExportImageService(
|
exportImageService = new ExportImageService(
|
||||||
mockQ,
|
mockQ,
|
||||||
@@ -92,7 +95,8 @@ define(
|
|||||||
mockExportTimeoutConstant,
|
mockExportTimeoutConstant,
|
||||||
mockHtml2Canvas,
|
mockHtml2Canvas,
|
||||||
mockSaveAs,
|
mockSaveAs,
|
||||||
mockFileReader
|
mockFileReader,
|
||||||
|
mockChangeBackgroundColor
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,6 +119,28 @@ define(
|
|||||||
expect(mockSaveAs).toHaveBeenCalled();
|
expect(mockSaveAs).toHaveBeenCalled();
|
||||||
expect(mockPromise.finally).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", 'white');
|
||||||
|
|
||||||
|
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'white');
|
||||||
|
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'black');
|
||||||
|
|
||||||
|
exportImageService.exportJPG(testElement, "plot.jpg", 'white');
|
||||||
|
|
||||||
|
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'white');
|
||||||
|
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'black');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not change background color when color is not specified in parameters", function () {
|
||||||
|
exportImageService.exportPNG(testElement, "plot.png");
|
||||||
|
|
||||||
|
expect(mockChangeBackgroundColor).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
exportImageService.exportJPG(testElement, "plot.jpg");
|
||||||
|
|
||||||
|
expect(mockChangeBackgroundColor).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -76,20 +76,22 @@ define([
|
|||||||
throw new Error('Event not supported by composition: ' + event);
|
throw new Error('Event not supported by composition: ' + event);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'add') {
|
if (this.provider.on && this.provider.off) {
|
||||||
this.provider.on(
|
if (event === 'add') {
|
||||||
this.domainObject,
|
this.provider.on(
|
||||||
'add',
|
this.domainObject,
|
||||||
this.onProviderAdd,
|
'add',
|
||||||
this
|
this.onProviderAdd,
|
||||||
);
|
this
|
||||||
} if (event === 'remove') {
|
);
|
||||||
this.provider.on(
|
} if (event === 'remove') {
|
||||||
this.domainObject,
|
this.provider.on(
|
||||||
'remove',
|
this.domainObject,
|
||||||
this.onProviderRemove,
|
'remove',
|
||||||
this
|
this.onProviderRemove,
|
||||||
);
|
this
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listeners[event].push({
|
this.listeners[event].push({
|
||||||
@@ -124,20 +126,22 @@ define([
|
|||||||
if (this.listeners[event].length === 0) {
|
if (this.listeners[event].length === 0) {
|
||||||
// Remove provider listener if this is the last callback to
|
// Remove provider listener if this is the last callback to
|
||||||
// be removed.
|
// be removed.
|
||||||
if (event === 'add') {
|
if (this.provider.off && this.provider.on) {
|
||||||
this.provider.off(
|
if (event === 'add') {
|
||||||
this.domainObject,
|
this.provider.off(
|
||||||
'add',
|
this.domainObject,
|
||||||
this.onProviderAdd,
|
'add',
|
||||||
this
|
this.onProviderAdd,
|
||||||
);
|
this
|
||||||
} else if (event === 'remove') {
|
);
|
||||||
this.provider.off(
|
} else if (event === 'remove') {
|
||||||
this.domainObject,
|
this.provider.off(
|
||||||
'remove',
|
this.domainObject,
|
||||||
this.onProviderRemove,
|
'remove',
|
||||||
this
|
this.onProviderRemove,
|
||||||
);
|
this
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -206,6 +206,17 @@ define([], function () {
|
|||||||
getDescription: function () {
|
getDescription: function () {
|
||||||
return ' is undefined';
|
return ' is undefined';
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
isDefined: {
|
||||||
|
operation: function (input) {
|
||||||
|
return typeof input[0] !== 'undefined';
|
||||||
|
},
|
||||||
|
text: 'is defined',
|
||||||
|
appliesTo: ['string', 'number'],
|
||||||
|
inputCount: 0,
|
||||||
|
getDescription: function () {
|
||||||
|
return ' is defined';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -304,9 +315,12 @@ define([], function () {
|
|||||||
op = this.operations[operation] && this.operations[operation].operation;
|
op = this.operations[operation] && this.operations[operation].operation;
|
||||||
input = telemetryValue && telemetryValue.concat(values);
|
input = telemetryValue && telemetryValue.concat(values);
|
||||||
validator = op && this.inputValidators[this.operations[operation].appliesTo[0]];
|
validator = op && this.inputValidators[this.operations[operation].appliesTo[0]];
|
||||||
|
|
||||||
if (op && input && validator) {
|
if (op && input && validator) {
|
||||||
return validator(input) && op(input);
|
if (this.operations[operation].appliesTo.length === 2) {
|
||||||
|
return (this.validateNumberInput(input) || this.validateStringInput(input)) && op(input);
|
||||||
|
} else {
|
||||||
|
return validator(input) && op(input);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Malformed condition');
|
throw new Error('Malformed condition');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ define([
|
|||||||
'./TestDataManager',
|
'./TestDataManager',
|
||||||
'./WidgetDnD',
|
'./WidgetDnD',
|
||||||
'./eventHelpers',
|
'./eventHelpers',
|
||||||
|
'../../../api/objects/object-utils',
|
||||||
'lodash',
|
'lodash',
|
||||||
'zepto'
|
'zepto'
|
||||||
], function (
|
], function (
|
||||||
@@ -14,6 +15,7 @@ define([
|
|||||||
TestDataManager,
|
TestDataManager,
|
||||||
WidgetDnD,
|
WidgetDnD,
|
||||||
eventHelpers,
|
eventHelpers,
|
||||||
|
objectUtils,
|
||||||
_,
|
_,
|
||||||
$
|
$
|
||||||
) {
|
) {
|
||||||
@@ -77,7 +79,7 @@ define([
|
|||||||
this.addHyperlink(domainObject.url, domainObject.openNewTab);
|
this.addHyperlink(domainObject.url, domainObject.openNewTab);
|
||||||
this.watchForChanges(openmct, domainObject);
|
this.watchForChanges(openmct, domainObject);
|
||||||
|
|
||||||
var id = this.domainObject.identifier.key,
|
var id = objectUtils.makeKeyString(this.domainObject.identifier),
|
||||||
self = this,
|
self = this,
|
||||||
oldDomainObject,
|
oldDomainObject,
|
||||||
statusCapability;
|
statusCapability;
|
||||||
|
|||||||
@@ -325,6 +325,10 @@ define(['../src/ConditionEvaluator'], function (ConditionEvaluator) {
|
|||||||
testOperation = testEvaluator.operations.isUndefined.operation;
|
testOperation = testEvaluator.operations.isUndefined.operation;
|
||||||
expect(testOperation([1])).toEqual(false);
|
expect(testOperation([1])).toEqual(false);
|
||||||
expect(testOperation([])).toEqual(true);
|
expect(testOperation([])).toEqual(true);
|
||||||
|
//isDefined
|
||||||
|
testOperation = testEvaluator.operations.isDefined.operation;
|
||||||
|
expect(testOperation([1])).toEqual(true);
|
||||||
|
expect(testOperation([])).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can produce a description for all supported operations', function () {
|
it('can produce a description for all supported operations', function () {
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
|
|||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockDomainObject = {
|
mockDomainObject = {
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'testKey'
|
key: 'testKey',
|
||||||
|
namespace: 'testNamespace'
|
||||||
},
|
},
|
||||||
name: 'testName',
|
name: 'testName',
|
||||||
composition: [],
|
composition: [],
|
||||||
@@ -49,7 +50,7 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
|
|||||||
mockObjectService.getObjects = jasmine.createSpy('objectService');
|
mockObjectService.getObjects = jasmine.createSpy('objectService');
|
||||||
mockObjectService.getObjects.andReturn(new Promise(function (resolve, reject) {
|
mockObjectService.getObjects.andReturn(new Promise(function (resolve, reject) {
|
||||||
resolve({
|
resolve({
|
||||||
testKey: mockOldDomainObject
|
'testNamespace:testKey': mockOldDomainObject
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
mockOpenMCT = jasmine.createSpyObj('openmct', [
|
mockOpenMCT = jasmine.createSpyObj('openmct', [
|
||||||
@@ -73,6 +74,10 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
|
|||||||
summaryWidget.show(mockContainer);
|
summaryWidget.show(mockContainer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('queries with legacyId', function () {
|
||||||
|
expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']);
|
||||||
|
});
|
||||||
|
|
||||||
it('adds its DOM element to the view', function () {
|
it('adds its DOM element to the view', function () {
|
||||||
expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0);
|
expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ define(['EventEmitter'], function (EventEmitter) {
|
|||||||
element.addEventListener('click', selectCapture);
|
element.addEventListener('click', selectCapture);
|
||||||
|
|
||||||
if (select) {
|
if (select) {
|
||||||
this.select(selectable);
|
element.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
return function () {
|
return function () {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ requirejs.config({
|
|||||||
"vue": "node_modules/vue/dist/vue.min",
|
"vue": "node_modules/vue/dist/vue.min",
|
||||||
"zepto": "bower_components/zepto/zepto.min",
|
"zepto": "bower_components/zepto/zepto.min",
|
||||||
"lodash": "bower_components/lodash/lodash",
|
"lodash": "bower_components/lodash/lodash",
|
||||||
"d3-selection": "node_modules/d3-selection/build/d3-selection.min",
|
"d3-selection": "node_modules/d3-selection/dist/d3-selection.min",
|
||||||
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
|
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
|
||||||
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
|
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
|
||||||
"d3-array": "node_modules/d3-array/build/d3-array.min",
|
"d3-array": "node_modules/d3-array/build/d3-array.min",
|
||||||
|
|||||||
Reference in New Issue
Block a user