Compare commits
27 Commits
indicators
...
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 | ||
|
|
1ad5094b72 | ||
|
|
b54ee2257e | ||
|
|
fcef4274e5 | ||
|
|
744a5340d3 | ||
|
|
d140051054 | ||
|
|
8da74f2665 |
@@ -21,5 +21,6 @@
|
||||
"shadow": "outer",
|
||||
"strict": "implied",
|
||||
"undef": true,
|
||||
"unused": "vars"
|
||||
"unused": "vars",
|
||||
"latedef": "nofunc"
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ and [`gulp`](http://gulpjs.com/).
|
||||
|
||||
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.
|
||||
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:
|
||||
production:
|
||||
branch: master
|
||||
@@ -16,4 +24,4 @@ test:
|
||||
general:
|
||||
branches:
|
||||
ignore:
|
||||
- gh-pages
|
||||
- gh-pages
|
||||
@@ -2283,7 +2283,7 @@ To install build dependencies (only needs to be run once):
|
||||
|
||||
To build:
|
||||
|
||||
`npm run prepublish`
|
||||
`npm run prepare`
|
||||
|
||||
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
|
||||
|
||||
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*/
|
||||
|
||||
require("v8-compile-cache");
|
||||
|
||||
var gulp = require('gulp'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
path = require('path'),
|
||||
@@ -177,4 +179,4 @@ gulp.task('install', [ 'assets', 'scripts' ]);
|
||||
|
||||
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",
|
||||
"zepto": "bower_components/zepto/zepto.min",
|
||||
"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-axis": "node_modules/d3-axis/build/d3-axis.min",
|
||||
"d3-array": "node_modules/d3-array/build/d3-array.min",
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
"moment": "^2.11.1",
|
||||
"node-bourbon": "^4.2.3",
|
||||
"requirejs": "2.1.x",
|
||||
"split": "^1.0.0"
|
||||
"split": "^1.0.0",
|
||||
"v8-compile-cache": "^1.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
@@ -60,7 +61,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",
|
||||
"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": {
|
||||
"type": "git",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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 &&
|
||||
@@ -59,10 +49,7 @@ define(
|
||||
|
||||
var removeCheck = navigationService
|
||||
.checkBeforeNavigation(function () {
|
||||
if (isDirty(domainObject)) {
|
||||
return "Continuing will cause the loss of any unsaved changes.";
|
||||
}
|
||||
return false;
|
||||
return "Continuing will cause the loss of any unsaved changes.";
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
|
||||
@@ -52,8 +52,20 @@ define(
|
||||
}
|
||||
|
||||
function setSelection(selection) {
|
||||
self.scope.selection = selection;
|
||||
self.refreshComposition(selection);
|
||||
if (!selection[0]) {
|
||||
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;
|
||||
@@ -70,15 +82,11 @@ define(
|
||||
/**
|
||||
* Gets the composition for the selected object and populates the scope with it.
|
||||
*
|
||||
* @param selection the selection object
|
||||
* @param domainObject the selected object
|
||||
* @private
|
||||
*/
|
||||
ElementsController.prototype.refreshComposition = function (selection) {
|
||||
if (!selection[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedObjectComposition = selection[0].context.oldItem.useCapability('composition');
|
||||
ElementsController.prototype.refreshComposition = function (domainObject) {
|
||||
var selectedObjectComposition = domainObject.useCapability('composition');
|
||||
|
||||
if (selectedObjectComposition) {
|
||||
selectedObjectComposition.then(function (composition) {
|
||||
|
||||
@@ -44,11 +44,17 @@ define(
|
||||
this.selectedObj = undefined;
|
||||
|
||||
openmct.selection.on('change', function (selection) {
|
||||
if (selection[0] && selection[0].context.toolbar) {
|
||||
this.select(selection[0].context.toolbar);
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -104,10 +104,10 @@ define(
|
||||
mockEditorCapability.isEditContextRoot.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);
|
||||
expect(checkFn()).toBe(false);
|
||||
expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes.");
|
||||
|
||||
mockEditorCapability.dirty.andReturn(true);
|
||||
expect(checkFn())
|
||||
|
||||
@@ -29,9 +29,25 @@ define(
|
||||
var mockScope,
|
||||
mockOpenMCT,
|
||||
mockSelection,
|
||||
mockDomainObject,
|
||||
mockMutationCapability,
|
||||
mockUnlisten,
|
||||
selectable = [],
|
||||
controller;
|
||||
|
||||
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']);
|
||||
mockSelection = jasmine.createSpyObj("selection", [
|
||||
'on',
|
||||
@@ -43,6 +59,14 @@ define(
|
||||
selection: mockSelection
|
||||
};
|
||||
|
||||
selectable[0] = {
|
||||
context: {
|
||||
oldItem: mockDomainObject
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(ElementsController.prototype, 'refreshComposition');
|
||||
|
||||
controller = new ElementsController(mockScope, mockOpenMCT);
|
||||
});
|
||||
|
||||
@@ -75,6 +99,34 @@ define(
|
||||
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;
|
||||
$bc: $colorInteriorBorder;
|
||||
&.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
|
||||
&:not(.no-frame) {
|
||||
background: $colorBodyBg;
|
||||
@@ -91,7 +90,7 @@
|
||||
|
||||
&.no-frame {
|
||||
background: transparent !important;
|
||||
border: none;
|
||||
border-color: transparent;
|
||||
.object-browse-bar .right {
|
||||
$m: 0;
|
||||
background: rgba(black, 0.3);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
.s-hover-border {
|
||||
border: 1px solid transparent;
|
||||
&:hover {
|
||||
border-color: rgba($colorSelectableSelectedPrimary, 0.5) !important;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ 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()) {
|
||||
|
||||
@@ -50,7 +50,11 @@ define(
|
||||
view.show(container);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ define(
|
||||
|
||||
FollowIndicator.prototype.getText = function () {
|
||||
var timer = this.timerService.getTimer();
|
||||
return (timer) ? 'Following timer ' + timer.getModel().name : NO_TIMER;
|
||||
return timer ? ('Following timer ' + timer.name) : NO_TIMER;
|
||||
};
|
||||
|
||||
FollowIndicator.prototype.getDescription = function () {
|
||||
|
||||
@@ -42,18 +42,15 @@ define(["../../src/indicators/FollowIndicator"], function (FollowIndicator) {
|
||||
});
|
||||
|
||||
describe("when a timer is set", function () {
|
||||
var testModel;
|
||||
var mockDomainObject;
|
||||
var testObject;
|
||||
|
||||
beforeEach(function () {
|
||||
testModel = { name: "some timer!" };
|
||||
mockDomainObject = jasmine.createSpyObj('timer', ['getModel']);
|
||||
mockDomainObject.getModel.andReturn(testModel);
|
||||
mockTimerService.getTimer.andReturn(mockDomainObject);
|
||||
testObject = { name: "some timer!" };
|
||||
mockTimerService.getTimer.andReturn(testObject);
|
||||
});
|
||||
|
||||
it("displays the timer's name", function () {
|
||||
expect(indicator.getText().indexOf(testModel.name))
|
||||
expect(indicator.getText().indexOf(testObject.name))
|
||||
.not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -272,7 +272,8 @@ define([
|
||||
"$scope",
|
||||
"$q",
|
||||
"dialogService",
|
||||
"openmct"
|
||||
"openmct",
|
||||
"$element"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
ng-controller="FixedController as controller">
|
||||
|
||||
<!-- 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"
|
||||
ng-if="!controller.getGridSize()[0] < 3"
|
||||
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
|
||||
@@ -35,35 +35,28 @@
|
||||
<!-- 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"
|
||||
ng-click="controller.select(element, $event)">
|
||||
mct-selectable="controller.getContext(element)"
|
||||
mct-init-select="controller.shouldSelect(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.selected()">
|
||||
<span class="s-selected s-moveable" ng-if="controller.isElementSelected()">
|
||||
<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-up="controller.moveHandle().endDrag()"
|
||||
ng-style="controller.selected().style"
|
||||
ng-click="$event.stopPropagation()">
|
||||
mct-drag-up="controller.endDrag()"
|
||||
ng-style="controller.getSelectedElementStyle()">
|
||||
</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="handle.endDrag()"
|
||||
ng-click="$event.stopPropagation()">
|
||||
mct-drag-up="controller.endDrag(handle)">
|
||||
</div>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
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 {{childObject.getId() + '-' + $id}} t-object-type-{{ childObject.getModel().type }}"
|
||||
<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) }"
|
||||
ng-repeat="childObject in composition"
|
||||
ng-init="controller.selectIfNew(childObject.getId() + '-' + $id, childObject)"
|
||||
|
||||
@@ -47,7 +47,7 @@ define(
|
||||
* @constructor
|
||||
* @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.values = {}; // Cache values by ID
|
||||
this.elementProxiesById = {};
|
||||
@@ -55,9 +55,11 @@ 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;
|
||||
[
|
||||
@@ -87,9 +89,8 @@ define(
|
||||
|
||||
// Update the style for a selected element
|
||||
function updateSelectionStyle() {
|
||||
var element = self.selection && self.selection.get();
|
||||
if (element) {
|
||||
element.style = convertPosition(element);
|
||||
if (self.selectedElementProxy) {
|
||||
self.selectedElementProxy.style = convertPosition(self.selectedElementProxy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,25 +137,19 @@ define(
|
||||
|
||||
// Decorate elements in the current configuration
|
||||
function refreshElements() {
|
||||
// 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);
|
||||
}
|
||||
var elements = (($scope.configuration || {}).elements || []);
|
||||
|
||||
// Create the new proxies...
|
||||
self.elementProxies = elements.map(makeProxyElement);
|
||||
|
||||
// Clear old selection, and restore if appropriate
|
||||
if (self.selection) {
|
||||
self.selection.deselect();
|
||||
if (index > -1) {
|
||||
self.select(self.elementProxies[index]);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,12 +219,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.");
|
||||
@@ -263,21 +258,36 @@ 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;
|
||||
|
||||
// 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));
|
||||
this.updateSelectionStyle = updateSelectionStyle;
|
||||
|
||||
// Detect changes to grid size
|
||||
$scope.$watch("model.layoutGrid", updateElementPositions);
|
||||
@@ -298,10 +308,13 @@ 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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -492,42 +505,56 @@ define(
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the element is currently selected, or (if no
|
||||
* argument is supplied) get the currently selected element.
|
||||
* @returns {boolean} true if selected
|
||||
* Checks if the element should be selected or not.
|
||||
*
|
||||
* @param elementProxy the element to check
|
||||
* @returns {boolean} true if the element should be selected.
|
||||
*/
|
||||
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, 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);
|
||||
FixedController.prototype.shouldSelect = function (elementProxy) {
|
||||
if (elementProxy.element === this.elementToSelectAfterRefresh) {
|
||||
delete this.elementToSelectAfterRefresh;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the current user selection.
|
||||
* Checks if an element is currently selected.
|
||||
*
|
||||
* @returns {boolean} true if an element is selected.
|
||||
*/
|
||||
FixedController.prototype.clearSelection = function () {
|
||||
if (this.selection) {
|
||||
this.selection.deselect();
|
||||
this.resizeHandles = [];
|
||||
this.mvHandle = undefined;
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -548,6 +575,38 @@ 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;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 startDrag() {
|
||||
FixedDragHandle.prototype.startDrag = function () {
|
||||
// Cache initial x/y positions
|
||||
this.dragging = {
|
||||
x: this.elementHandle.x(),
|
||||
|
||||
@@ -515,7 +515,7 @@ define(
|
||||
LayoutController.prototype.selectIfNew = function (selector, domainObject) {
|
||||
if (domainObject.getId() === this.droppedIdToSelectAfterRefresh) {
|
||||
setTimeout(function () {
|
||||
$('.' + selector)[0].click();
|
||||
$('[data-layout-id="' + selector + '"]')[0].click();
|
||||
delete this.droppedIdToSelectAfterRefresh;
|
||||
}.bind(this), 0);
|
||||
}
|
||||
|
||||
@@ -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 {number[]} gridSize the current layout grid size in [x,y] from
|
||||
* @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) {
|
||||
/**
|
||||
|
||||
@@ -21,8 +21,14 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/FixedController"],
|
||||
function (FixedController) {
|
||||
[
|
||||
"../src/FixedController",
|
||||
"zepto"
|
||||
],
|
||||
function (
|
||||
FixedController,
|
||||
$
|
||||
) {
|
||||
|
||||
describe("The Fixed Position controller", function () {
|
||||
var mockScope,
|
||||
@@ -46,6 +52,9 @@ define(
|
||||
mockMetadata,
|
||||
mockTimeSystem,
|
||||
mockLimitEvaluator,
|
||||
mockSelection,
|
||||
$element = [],
|
||||
selectable = [],
|
||||
controller;
|
||||
|
||||
// Utility function; find a watch for a given expression
|
||||
@@ -180,17 +189,30 @@ define(
|
||||
|
||||
mockScope.model = testModel;
|
||||
mockScope.configuration = testConfiguration;
|
||||
mockScope.selection = jasmine.createSpyObj(
|
||||
'selection',
|
||||
['select', 'get', 'selected', 'deselect', 'proxy']
|
||||
);
|
||||
|
||||
selectable[0] = {
|
||||
context: {
|
||||
oldItem: mockDomainObject
|
||||
}
|
||||
};
|
||||
mockSelection = jasmine.createSpyObj("selection", [
|
||||
'select',
|
||||
'on',
|
||||
'off',
|
||||
'get'
|
||||
]);
|
||||
mockSelection.get.andCallThrough();
|
||||
|
||||
mockOpenMCT = {
|
||||
time: mockConductor,
|
||||
telemetry: mockTelemetryAPI,
|
||||
composition: mockCompositionAPI
|
||||
composition: mockCompositionAPI,
|
||||
selection: mockSelection
|
||||
};
|
||||
|
||||
$element = $('<div></div>');
|
||||
spyOn($element[0], 'click');
|
||||
|
||||
mockMetadata = jasmine.createSpyObj('mockMetadata', [
|
||||
'valuesForHints',
|
||||
'value',
|
||||
@@ -226,11 +248,11 @@ define(
|
||||
mockScope,
|
||||
mockQ,
|
||||
mockDialogService,
|
||||
mockOpenMCT
|
||||
mockOpenMCT,
|
||||
$element
|
||||
);
|
||||
|
||||
findWatch("model.layoutGrid")(testModel.layoutGrid);
|
||||
findWatch("selection")(mockScope.selection);
|
||||
});
|
||||
|
||||
it("subscribes when a domain object is available", function () {
|
||||
@@ -306,41 +328,41 @@ define(
|
||||
});
|
||||
|
||||
it("allows elements to be selected", function () {
|
||||
var elements;
|
||||
|
||||
testModel.modified = 1;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
elements = controller.getElements();
|
||||
controller.select(elements[1]);
|
||||
expect(mockScope.selection.select)
|
||||
.toHaveBeenCalledWith(elements[1]);
|
||||
selectable[0].context.elementProxy = controller.getElements()[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(controller.isElementSelected()).toBe(true);
|
||||
});
|
||||
|
||||
it("allows selection retrieval", function () {
|
||||
// selected with no arguments should give the current
|
||||
// selection
|
||||
var elements;
|
||||
|
||||
testModel.modified = 1;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
elements = controller.getElements();
|
||||
controller.select(elements[1]);
|
||||
mockScope.selection.get.andReturn(elements[1]);
|
||||
expect(controller.selected()).toEqual(elements[1]);
|
||||
selectable[0].context.elementProxy = elements[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(controller.getSelectedElement()).toEqual(elements[1]);
|
||||
});
|
||||
|
||||
it("allows selections to be cleared", function () {
|
||||
var elements;
|
||||
|
||||
it("selects the parent view when selected element is removed", function () {
|
||||
testModel.modified = 1;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
elements = controller.getElements();
|
||||
controller.select(elements[1]);
|
||||
controller.clearSelection();
|
||||
expect(controller.selected(elements[1])).toBeFalsy();
|
||||
var elements = controller.getElements();
|
||||
selectable[0].context.elementProxy = elements[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
elements[1].remove();
|
||||
testModel.modified = 2;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
expect($element[0].click).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retains selections during refresh", function () {
|
||||
@@ -352,23 +374,21 @@ define(
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
elements = controller.getElements();
|
||||
controller.select(elements[1]);
|
||||
selectable[0].context.elementProxy = elements[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
// Verify precondition
|
||||
expect(mockScope.selection.select.calls.length).toEqual(1);
|
||||
|
||||
// Mimic selection behavior
|
||||
mockScope.selection.get.andReturn(elements[1]);
|
||||
expect(controller.getSelectedElement()).toEqual(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(mockScope.selection.select.calls.length).toEqual(2);
|
||||
expect(controller.shouldSelect(elements[1])).toBe(true);
|
||||
});
|
||||
|
||||
it("Displays received values for telemetry elements", function () {
|
||||
@@ -505,21 +525,25 @@ define(
|
||||
});
|
||||
|
||||
it("exposes a view-level selection proxy", function () {
|
||||
expect(mockScope.selection.proxy).toHaveBeenCalledWith(
|
||||
jasmine.any(Object)
|
||||
);
|
||||
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();
|
||||
});
|
||||
|
||||
it("exposes drag handles", function () {
|
||||
var handles;
|
||||
|
||||
// Select something so that drag handles are expected
|
||||
testModel.modified = 1;
|
||||
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
|
||||
handles = controller.handles();
|
||||
|
||||
expect(handles).toEqual(jasmine.any(Array));
|
||||
expect(handles.length).not.toEqual(0);
|
||||
|
||||
@@ -532,15 +556,14 @@ 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);
|
||||
controller.select(controller.getElements()[1]);
|
||||
|
||||
selectable[0].context.elementProxy = controller.getElements()[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
// Should have a move handle
|
||||
handle = controller.moveHandle();
|
||||
var handle = controller.moveHandle();
|
||||
|
||||
// And it should have start/continue/end drag methods
|
||||
expect(handle.startDrag).toEqual(jasmine.any(Function));
|
||||
@@ -551,26 +574,40 @@ 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);
|
||||
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
|
||||
oldStyle = controller.selected().style;
|
||||
oldStyle = controller.getSelectedElementStyle();
|
||||
|
||||
// Start a drag gesture
|
||||
controller.moveHandle().startDrag();
|
||||
|
||||
// Haven't moved yet; style shouldn't have updated yet
|
||||
expect(controller.selected().style).toEqual(oldStyle);
|
||||
expect(controller.getSelectedElementStyle()).toEqual(oldStyle);
|
||||
|
||||
// Drag a little
|
||||
controller.moveHandle().continueDrag([1000, 100]);
|
||||
|
||||
// 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 () {
|
||||
@@ -702,6 +739,14 @@ 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)
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -475,11 +475,11 @@ define(
|
||||
);
|
||||
|
||||
var childObj = mockDomainObject("d");
|
||||
var testElement = $("<div class='some-class'></div>");
|
||||
var testElement = $("<div data-layout-id='some-id'></div>");
|
||||
$element.append(testElement);
|
||||
spyOn(testElement[0], 'click');
|
||||
|
||||
controller.selectIfNew('some-class', childObj);
|
||||
controller.selectIfNew('some-id', childObj);
|
||||
jasmine.Clock.tick(0);
|
||||
|
||||
expect(testElement[0].click).toHaveBeenCalled();
|
||||
|
||||
@@ -415,7 +415,7 @@ define(
|
||||
PlotController.prototype.exportPNG = function () {
|
||||
var self = this;
|
||||
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;
|
||||
});
|
||||
};
|
||||
@@ -426,7 +426,7 @@ define(
|
||||
PlotController.prototype.exportJPG = function () {
|
||||
var self = this;
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
function ExportImageService($q, $timeout, $log, EXPORT_IMAGE_TIMEOUT, injHtml2Canvas, injSaveAs, injFileReader, injChangeBackgroundColor) {
|
||||
self.$q = $q;
|
||||
self.$timeout = $timeout;
|
||||
self.$log = $log;
|
||||
@@ -51,6 +51,7 @@ define(
|
||||
self.html2canvas = injHtml2Canvas || html2canvas;
|
||||
self.saveAs = injSaveAs || saveAs;
|
||||
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
|
||||
* @returns {promise}
|
||||
*/
|
||||
function renderElement(element, type) {
|
||||
function renderElement(element, type, color) {
|
||||
var defer = self.$q.defer(),
|
||||
validTypes = ["png", "jpg", "jpeg"],
|
||||
renderTimeout;
|
||||
renderTimeout,
|
||||
originalColor;
|
||||
|
||||
if (validTypes.indexOf(type) === -1) {
|
||||
self.$log.error("Invalid type requested. Try: (" + validTypes.join(",") + ")");
|
||||
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 () {
|
||||
defer.reject("html2canvas timed out");
|
||||
self.$log.warn("html2canvas timed out");
|
||||
@@ -78,13 +88,15 @@ define(
|
||||
try {
|
||||
self.html2canvas(element, {
|
||||
onrendered: function (canvas) {
|
||||
if (color) {
|
||||
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;
|
||||
@@ -96,7 +108,13 @@ define(
|
||||
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;
|
||||
}
|
||||
@@ -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.
|
||||
* @param {node} element to be exported
|
||||
* @param {string} filename the exported image
|
||||
* @returns {promise}
|
||||
*/
|
||||
ExportImageService.prototype.exportJPG = function (element, filename) {
|
||||
return renderElement(element, "jpeg").then(function (img) {
|
||||
ExportImageService.prototype.exportJPG = function (element, filename, color) {
|
||||
return renderElement(element, "jpeg", color).then(function (img) {
|
||||
self.saveAs(img, filename);
|
||||
});
|
||||
};
|
||||
@@ -143,8 +168,8 @@ define(
|
||||
* @param {string} filename the exported image
|
||||
* @returns {promise}
|
||||
*/
|
||||
ExportImageService.prototype.exportPNG = function (element, filename) {
|
||||
return renderElement(element, "png").then(function (img) {
|
||||
ExportImageService.prototype.exportPNG = function (element, filename, color) {
|
||||
return renderElement(element, "png", color).then(function (img) {
|
||||
self.saveAs(img, filename);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -37,7 +37,8 @@ define(
|
||||
mockFileReader,
|
||||
mockExportTimeoutConstant,
|
||||
testElement,
|
||||
exportImageService;
|
||||
exportImageService,
|
||||
mockChangeBackgroundColor;
|
||||
|
||||
describe("ExportImageService", function () {
|
||||
beforeEach(function () {
|
||||
@@ -83,7 +84,9 @@ define(
|
||||
["readAsDataURL", "onloadend"]
|
||||
);
|
||||
mockExportTimeoutConstant = 0;
|
||||
testElement = {};
|
||||
testElement = {style: {backgroundColor: 'black'}};
|
||||
|
||||
mockChangeBackgroundColor = jasmine.createSpy('changeBackgroundColor');
|
||||
|
||||
exportImageService = new ExportImageService(
|
||||
mockQ,
|
||||
@@ -92,7 +95,8 @@ define(
|
||||
mockExportTimeoutConstant,
|
||||
mockHtml2Canvas,
|
||||
mockSaveAs,
|
||||
mockFileReader
|
||||
mockFileReader,
|
||||
mockChangeBackgroundColor
|
||||
);
|
||||
});
|
||||
|
||||
@@ -115,6 +119,28 @@ 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", '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();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
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">
|
||||
|
||||
@@ -76,20 +76,22 @@ define([
|
||||
throw new Error('Event not supported by composition: ' + event);
|
||||
}
|
||||
|
||||
if (event === 'add') {
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
} if (event === 'remove') {
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
if (this.provider.on && this.provider.off) {
|
||||
if (event === 'add') {
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
} if (event === 'remove') {
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners[event].push({
|
||||
@@ -124,20 +126,22 @@ define([
|
||||
if (this.listeners[event].length === 0) {
|
||||
// Remove provider listener if this is the last callback to
|
||||
// be removed.
|
||||
if (event === 'add') {
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
} else if (event === 'remove') {
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
if (this.provider.off && this.provider.on) {
|
||||
if (event === 'add') {
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
} else if (event === 'remove') {
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -206,6 +206,17 @@ define([], function () {
|
||||
getDescription: function () {
|
||||
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;
|
||||
input = telemetryValue && telemetryValue.concat(values);
|
||||
validator = op && this.inputValidators[this.operations[operation].appliesTo[0]];
|
||||
|
||||
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 {
|
||||
throw new Error('Malformed condition');
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ define([
|
||||
'./TestDataManager',
|
||||
'./WidgetDnD',
|
||||
'./eventHelpers',
|
||||
'../../../api/objects/object-utils',
|
||||
'lodash',
|
||||
'zepto'
|
||||
], function (
|
||||
@@ -14,6 +15,7 @@ define([
|
||||
TestDataManager,
|
||||
WidgetDnD,
|
||||
eventHelpers,
|
||||
objectUtils,
|
||||
_,
|
||||
$
|
||||
) {
|
||||
@@ -77,7 +79,7 @@ define([
|
||||
this.addHyperlink(domainObject.url, domainObject.openNewTab);
|
||||
this.watchForChanges(openmct, domainObject);
|
||||
|
||||
var id = this.domainObject.identifier.key,
|
||||
var id = objectUtils.makeKeyString(this.domainObject.identifier),
|
||||
self = this,
|
||||
oldDomainObject,
|
||||
statusCapability;
|
||||
|
||||
@@ -325,6 +325,10 @@ define(['../src/ConditionEvaluator'], function (ConditionEvaluator) {
|
||||
testOperation = testEvaluator.operations.isUndefined.operation;
|
||||
expect(testOperation([1])).toEqual(false);
|
||||
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 () {
|
||||
|
||||
@@ -14,7 +14,8 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
|
||||
beforeEach(function () {
|
||||
mockDomainObject = {
|
||||
identifier: {
|
||||
key: 'testKey'
|
||||
key: 'testKey',
|
||||
namespace: 'testNamespace'
|
||||
},
|
||||
name: 'testName',
|
||||
composition: [],
|
||||
@@ -49,7 +50,7 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
|
||||
mockObjectService.getObjects = jasmine.createSpy('objectService');
|
||||
mockObjectService.getObjects.andReturn(new Promise(function (resolve, reject) {
|
||||
resolve({
|
||||
testKey: mockOldDomainObject
|
||||
'testNamespace:testKey': mockOldDomainObject
|
||||
});
|
||||
}));
|
||||
mockOpenMCT = jasmine.createSpyObj('openmct', [
|
||||
@@ -73,6 +74,10 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
|
||||
summaryWidget.show(mockContainer);
|
||||
});
|
||||
|
||||
it('queries with legacyId', function () {
|
||||
expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']);
|
||||
});
|
||||
|
||||
it('adds its DOM element to the view', function () {
|
||||
expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -115,7 +115,7 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
element.addEventListener('click', selectCapture);
|
||||
|
||||
if (select) {
|
||||
this.select(selectable);
|
||||
element.click();
|
||||
}
|
||||
|
||||
return function () {
|
||||
|
||||
@@ -66,7 +66,7 @@ requirejs.config({
|
||||
"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",
|
||||
"d3-selection": "node_modules/d3-selection/dist/d3-selection.min",
|
||||
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
|
||||
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
|
||||
"d3-array": "node_modules/d3-array/build/d3-array.min",
|
||||
|
||||
Reference in New Issue
Block a user