Compare commits

..

15 Commits

Author SHA1 Message Date
Pete Richards
d475d767d5 add grootprovider 2016-06-17 17:05:05 -07:00
Pete Richards
a63e053399 [ObjectAPI] Draft new Object API
Rought prototype of new object API.
2016-06-17 16:59:35 -07:00
Victor Woeltjen
5de7a96ccc Merge pull request #1010 from nasa/api-type-proto
[API Prototype] Type registration
2016-06-17 10:18:42 -07:00
Victor Woeltjen
09a833f524 Merge branch 'api-tutorials' into api-type-proto 2016-06-10 13:28:09 -07:00
Victor Woeltjen
9c4e17bfab [Tutorials] Add telemetry tutorial 2016-06-07 13:14:36 -07:00
Victor Woeltjen
d3e5d95d6b [Tutorials] Add example server 2016-06-07 13:00:38 -07:00
Victor Woeltjen
c70793ac2d [Tutorials] Add remainder of bargraph 2016-06-07 12:55:29 -07:00
Victor Woeltjen
a6ef1d3423 [Tutorials] Add Bar Graph tutorial 2016-06-07 12:49:38 -07:00
Victor Woeltjen
c4fec1af6a [API] Move type toward a newer API 2016-05-27 13:31:30 -07:00
Victor Woeltjen
a6996df3df [API] Begin moving out type 2016-05-27 13:17:16 -07:00
Victor Woeltjen
0c660238f2 [API] Add MCT class 2016-05-27 11:49:43 -07:00
Victor Woeltjen
b73b824e55 [API] Add EventEmitter dep 2016-05-27 11:45:59 -07:00
Victor Woeltjen
1954d98628 [Tutorials] Remove diff markings in TodoController 2016-05-27 11:30:53 -07:00
Victor Woeltjen
7aa034ce23 Add todo tutorial 2016-05-26 16:05:38 -07:00
Victor Woeltjen
385dc5d298 Begin adding tutorials 2016-05-26 15:36:09 -07:00
110 changed files with 3890 additions and 3991 deletions

View File

@@ -1,4 +1,4 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0)
# Open MCT
Open MCT is a web-based platform for mission operations user interface
software.
@@ -7,7 +7,7 @@ software.
A bundle is a group of software components (including source code, declared
as AMD modules, as well as resources such as images and HTML templates)
that is intended to be added or removed as a single unit. A plug-in for
that are intended to be added or removed as a single unit. A plug-in for
Open MCT will be expressed as a bundle; platform components are also
expressed as bundles.
@@ -133,6 +133,6 @@ documentation, may presume an understanding of these terms.
it, and it is thereafter considered the _navigated_ object (until the
user makes another such choice.)
* _space_: A name used to identify a persistence store. Interactions with
persistence will generally involve a `space` parameter in some form, to
persistence with generally involve a `space` parameter in some form, to
distinguish multiple persistence stores from one another (for cases
where there are multiple valid persistence locations available.)

8
app.js
View File

@@ -75,8 +75,6 @@
// Expose everything else as static files
app.use(express['static'](options.directory));
// Finally, open the HTTP server and log the instance to the console
app.listen(options.port, function() {
console.log('Open MCT application running at localhost:' + options.port)
});
}());
// Finally, open the HTTP server
app.listen(options.port);
}());

View File

@@ -18,6 +18,8 @@
"node-uuid": "^1.4.7",
"comma-separated-values": "^3.6.4",
"FileSaver.js": "^0.0.2",
"zepto": "^1.1.6"
"zepto": "^1.1.6",
"eventemitter3": "^1.2.0",
"lodash": "3.10.1"
}
}

View File

@@ -31,10 +31,17 @@
<script type="text/javascript">
require(['main'], function (mct) {
require([
'./tutorials/grootprovider/groots',
'./tutorials/todo/todo',
'./tutorials/todo/bundle',
'./example/imagery/bundle',
'./example/eventGenerator/bundle',
'./example/generator/bundle'
], mct.run.bind(mct));
'./example/generator/bundle',
], function (grootify, todoPlugin) {
grootify(mct);
todoPlugin(mct);
mct.start();
})
});
</script>
<link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css">

25
main.js
View File

@@ -28,13 +28,15 @@ requirejs.config({
"angular-route": "bower_components/angular-route/angular-route.min",
"csv": "bower_components/comma-separated-values/csv.min",
"es6-promise": "bower_components/es6-promise/promise.min",
"EventEmitter": "bower_components/eventemitter3/index",
"moment": "bower_components/moment/moment",
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
"screenfull": "bower_components/screenfull/dist/screenfull.min",
"text": "bower_components/text/text",
"uuid": "bower_components/node-uuid/uuid",
"zepto": "bower_components/zepto/zepto.min"
"zepto": "bower_components/zepto/zepto.min",
"lodash": "bower_components/lodash/lodash"
},
"shim": {
"angular": {
@@ -43,6 +45,9 @@ requirejs.config({
"angular-route": {
"deps": ["angular"]
},
"EventEmitter": {
"exports": "EventEmitter"
},
"moment-duration-format": {
"deps": ["moment"]
},
@@ -58,6 +63,7 @@ requirejs.config({
define([
'./platform/framework/src/Main',
'legacyRegistry',
'./src/MCT',
'./platform/framework/bundle',
'./platform/core/bundle',
@@ -93,11 +99,14 @@ define([
'./platform/search/bundle',
'./platform/status/bundle',
'./platform/commonUI/regions/bundle'
], function (Main, legacyRegistry) {
return {
legacyRegistry: legacyRegistry,
run: function () {
return new Main().run(legacyRegistry);
}
};
], function (Main, legacyRegistry, MCT) {
var mct = new MCT();
mct.legacyRegistry = legacyRegistry;
mct.run = mct.start;
mct.on('start', function () {
return new Main().run(legacyRegistry);
});
return mct;
});

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "0.10.3",
"version": "0.10.2-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {
"express": "^4.13.1",

View File

@@ -27,7 +27,6 @@ define([
"./src/MenuArrowController",
"./src/navigation/NavigationService",
"./src/navigation/NavigateAction",
"./src/navigation/OrphanNavigationHandler",
"./src/windowing/NewTabAction",
"./src/windowing/FullscreenAction",
"./src/windowing/WindowTitler",
@@ -48,7 +47,6 @@ define([
MenuArrowController,
NavigationService,
NavigateAction,
OrphanNavigationHandler,
NewTabAction,
FullscreenAction,
WindowTitler,
@@ -93,9 +91,11 @@ define([
"$scope",
"$route",
"$location",
"$window",
"objectService",
"navigationService",
"urlService",
"policyService",
"DEFAULT_PATH"
]
},
@@ -199,9 +199,7 @@ define([
"implementation": NavigateAction,
"depends": [
"navigationService",
"$q",
"policyService",
"$window"
"$q"
]
},
{
@@ -255,14 +253,6 @@ define([
"$rootScope",
"$document"
]
},
{
"implementation": OrphanNavigationHandler,
"depends": [
"throttle",
"topic",
"navigationService"
]
}
],
"licenses": [

View File

@@ -44,9 +44,11 @@ define(
$scope,
$route,
$location,
$window,
objectService,
navigationService,
urlService,
policyService,
defaultPath
) {
var path = [ROOT_ID].concat(
@@ -73,10 +75,25 @@ define(
}
function setScopeObjects(domainObject, navigationAllowed) {
// Callback for updating the in-scope reference to the object
// that is currently navigated-to.
function setNavigation(domainObject) {
var navigationAllowed = true;
if (domainObject === $scope.navigatedObject) {
//do nothing;
return;
}
policyService.allow("navigation", $scope.navigatedObject, domainObject, function (message) {
navigationAllowed = $window.confirm(message + "\r\n\r\n" +
" Are you sure you want to continue?");
});
if (navigationAllowed) {
$scope.navigatedObject = domainObject;
$scope.treeModel.selectedObject = domainObject;
navigationService.setNavigation(domainObject);
updateRoute(domainObject);
} else {
//If navigation was unsuccessful (ie. blocked), reset
@@ -86,20 +103,6 @@ define(
}
}
// Callback for updating the in-scope reference to the object
// that is currently navigated-to.
function setNavigation(domainObject) {
if (domainObject === $scope.navigatedObject) {
//do nothing;
return;
}
if (domainObject) {
domainObject.getCapability("action").perform("navigate").then(setScopeObjects.bind(undefined, domainObject));
} else {
setScopeObjects(domainObject, true);
}
}
function navigateTo(domainObject) {
// Check if an object has been navigated-to already...

View File

@@ -33,12 +33,10 @@ define(
* @constructor
* @implements {Action}
*/
function NavigateAction(navigationService, $q, policyService, $window, context) {
function NavigateAction(navigationService, $q, context) {
this.domainObject = context.domainObject;
this.$q = $q;
this.navigationService = navigationService;
this.policyService = policyService;
this.$window = $window;
}
/**
@@ -47,20 +45,9 @@ define(
* navigation has been updated
*/
NavigateAction.prototype.perform = function () {
var self = this,
navigationAllowed = true;
function allow() {
self.policyService.allow("navigation", self.navigationService.getNavigation(), self.domainObject, function (message) {
navigationAllowed = self.$window.confirm(message + "\r\n\r\n" +
" Are you sure you want to continue?");
});
return navigationAllowed;
}
// Set navigation, and wrap like a promise
return this.$q.when(
allow() && this.navigationService.setNavigation(this.domainObject)
this.navigationService.setNavigation(this.domainObject)
);
};

View File

@@ -1,75 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* Navigates away from orphan objects whenever they are detected.
*
* An orphan object is an object whose apparent parent does not
* actually contain it. This may occur in certain circumstances, such
* as when persistence succeeds for a newly-created object but fails
* for its parent.
*
* @param throttle the `throttle` service
* @param topic the `topic` service
* @param navigationService the `navigationService`
* @constructor
*/
function OrphanNavigationHandler(throttle, topic, navigationService) {
var throttledCheckNavigation;
function getParent(domainObject) {
var context = domainObject.getCapability('context');
return context.getParent();
}
function isOrphan(domainObject) {
var parent = getParent(domainObject),
composition = parent.getModel().composition,
id = domainObject.getId();
return !composition || (composition.indexOf(id) === -1);
}
function navigateToParent(domainObject) {
var parent = getParent(domainObject);
return parent.getCapability('action').perform('navigate');
}
function checkNavigation() {
var navigatedObject = navigationService.getNavigation();
if (navigatedObject.hasCapability('context') &&
isOrphan(navigatedObject)) {
if (!navigatedObject.getCapability('editor').isEditContextRoot()) {
navigateToParent(navigatedObject);
}
}
}
throttledCheckNavigation = throttle(checkNavigation);
navigationService.addListener(throttledCheckNavigation);
topic('mutation').listen(throttledCheckNavigation);
}
return OrphanNavigationHandler;
});

View File

@@ -37,8 +37,9 @@ define(
mockUrlService,
mockDomainObject,
mockNextObject,
mockWindow,
mockPolicyService,
testDefaultRoot,
mockActionCapability,
controller;
function mockPromise(value) {
@@ -54,14 +55,25 @@ define(
mockScope,
mockRoute,
mockLocation,
mockWindow,
mockObjectService,
mockNavigationService,
mockUrlService,
mockPolicyService,
testDefaultRoot
);
}
beforeEach(function () {
mockWindow = jasmine.createSpyObj('$window', [
"confirm"
]);
mockWindow.confirm.andReturn(true);
mockPolicyService = jasmine.createSpyObj('policyService', [
'allow'
]);
testDefaultRoot = "some-root-level-domain-object";
mockScope = jasmine.createSpyObj(
@@ -116,8 +128,6 @@ define(
mockNextObject.getId.andReturn("next");
mockDomainObject.getId.andReturn(testDefaultRoot);
mockActionCapability = jasmine.createSpyObj('actionCapability', ['perform']);
instantiateController();
});
@@ -201,13 +211,8 @@ define(
mockContext.getPath.andReturn(
[mockRootObject, mockDomainObject, mockNextObject]
);
//Return true from navigate action
mockActionCapability.perform.andReturn(mockPromise(true));
mockNextObject.getCapability.andCallFake(function (c) {
return (c === 'context' && mockContext) ||
(c === 'action' && mockActionCapability);
return c === 'context' && mockContext;
});
mockScope.$on.andReturn(mockUnlisten);
// Provide a navigation change
@@ -220,7 +225,6 @@ define(
mockLocation.path.andReturn("/browse/");
mockNavigationService.setNavigation.andReturn(true);
mockActionCapability.perform.andReturn(mockPromise(true));
// Exercise the Angular workaround
mockNavigationService.addListener.mostRecentCall.args[0]();
@@ -239,9 +243,6 @@ define(
mockScope.navigatedObject = mockDomainObject;
mockNavigationService.setNavigation.andReturn(true);
mockActionCapability.perform.andReturn(mockPromise(true));
mockNextObject.getCapability.andReturn(mockActionCapability);
//Simulate a change in selected tree object
mockScope.treeModel = {selectedObject: mockDomainObject};
mockScope.$watch.mostRecentCall.args[1](mockNextObject);
@@ -253,10 +254,11 @@ define(
it("after failed navigation event resets the selected tree" +
" object", function () {
mockScope.navigatedObject = mockDomainObject;
//Return false from navigation action
mockActionCapability.perform.andReturn(mockPromise(false));
mockNextObject.getCapability.andReturn(mockActionCapability);
mockWindow.confirm.andReturn(false);
mockPolicyService.allow.andCallFake(function (category, object, context, callback) {
callback("unsaved changes");
return false;
});
//Simulate a change in selected tree object
mockScope.treeModel = {selectedObject: mockDomainObject};

View File

@@ -31,8 +31,6 @@ define(
var mockNavigationService,
mockQ,
mockDomainObject,
mockPolicyService,
mockWindow,
action;
function mockPromise(value) {
@@ -46,70 +44,25 @@ define(
beforeEach(function () {
mockNavigationService = jasmine.createSpyObj(
"navigationService",
[
"setNavigation",
"getNavigation"
]
["setNavigation"]
);
mockNavigationService.getNavigation.andReturn({});
mockQ = { when: mockPromise };
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getId", "getModel", "getCapability"]
);
mockPolicyService = jasmine.createSpyObj("policyService",
[
"allow"
]);
mockWindow = jasmine.createSpyObj("$window",
[
"confirm"
]);
action = new NavigateAction(
mockNavigationService,
mockQ,
mockPolicyService,
mockWindow,
{ domainObject: mockDomainObject }
);
});
it("invokes the policy service to determine if navigation" +
" allowed", function () {
it("invokes the navigate service when performed", function () {
action.perform();
expect(mockPolicyService.allow)
.toHaveBeenCalledWith("navigation", jasmine.any(Object), jasmine.any(Object), jasmine.any(Function));
});
it("prompts user if policy rejection", function () {
action.perform();
expect(mockPolicyService.allow).toHaveBeenCalled();
mockPolicyService.allow.mostRecentCall.args[3]();
expect(mockWindow.confirm).toHaveBeenCalled();
});
describe("shows a prompt", function () {
beforeEach(function () {
// Ensure the allow callback is called synchronously
mockPolicyService.allow.andCallFake(function () {
return arguments[3]();
});
});
it("does not navigate on prompt rejection", function () {
mockWindow.confirm.andReturn(false);
action.perform();
expect(mockNavigationService.setNavigation)
.not.toHaveBeenCalled();
});
it("does navigate on prompt acceptance", function () {
mockWindow.confirm.andReturn(true);
action.perform();
expect(mockNavigationService.setNavigation)
.toHaveBeenCalled();
});
expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockDomainObject);
});
it("is only applicable when a domain object is in context", function () {

View File

@@ -1,180 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'../../src/navigation/OrphanNavigationHandler'
], function (OrphanNavigationHandler) {
describe("OrphanNavigationHandler", function () {
var mockTopic,
mockThrottle,
mockMutationTopic,
mockNavigationService,
mockDomainObject,
mockParentObject,
mockContext,
mockActionCapability,
mockEditor,
testParentModel,
testId,
mockThrottledFns;
beforeEach(function () {
testId = 'some-identifier';
mockThrottledFns = [];
testParentModel = {};
mockTopic = jasmine.createSpy('topic');
mockThrottle = jasmine.createSpy('throttle');
mockNavigationService = jasmine.createSpyObj('navigationService', [
'getNavigation',
'addListener'
]);
mockMutationTopic = jasmine.createSpyObj('mutationTopic', [
'listen'
]);
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getId',
'getCapability',
'getModel',
'hasCapability'
]);
mockParentObject = jasmine.createSpyObj('domainObject', [
'getId',
'getCapability',
'getModel',
'hasCapability'
]);
mockContext = jasmine.createSpyObj('context', ['getParent']);
mockActionCapability = jasmine.createSpyObj('action', ['perform']);
mockEditor = jasmine.createSpyObj('editor', ['isEditContextRoot']);
mockThrottle.andCallFake(function (fn) {
var mockThrottledFn =
jasmine.createSpy('throttled-' + mockThrottledFns.length);
mockThrottledFn.andCallFake(fn);
mockThrottledFns.push(mockThrottledFn);
return mockThrottledFn;
});
mockTopic.andCallFake(function (k) {
return k === 'mutation' && mockMutationTopic;
});
mockDomainObject.getId.andReturn(testId);
mockDomainObject.getCapability.andCallFake(function (c) {
return {
context: mockContext,
editor: mockEditor
}[c];
});
mockDomainObject.hasCapability.andCallFake(function (c) {
return !!mockDomainObject.getCapability(c);
});
mockParentObject.getModel.andReturn(testParentModel);
mockParentObject.getCapability.andCallFake(function (c) {
return {
action: mockActionCapability
}[c];
});
mockContext.getParent.andReturn(mockParentObject);
mockNavigationService.getNavigation.andReturn(mockDomainObject);
mockEditor.isEditContextRoot.andReturn(false);
return new OrphanNavigationHandler(
mockThrottle,
mockTopic,
mockNavigationService
);
});
it("listens for mutation with a throttled function", function () {
expect(mockMutationTopic.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
expect(mockThrottledFns.indexOf(
mockMutationTopic.listen.mostRecentCall.args[0]
)).not.toEqual(-1);
});
it("listens for navigation changes with a throttled function", function () {
expect(mockNavigationService.addListener)
.toHaveBeenCalledWith(jasmine.any(Function));
expect(mockThrottledFns.indexOf(
mockNavigationService.addListener.mostRecentCall.args[0]
)).not.toEqual(-1);
});
[false, true].forEach(function (isOrphan) {
var prefix = isOrphan ? "" : "non-";
describe("for " + prefix + "orphan objects", function () {
beforeEach(function () {
testParentModel.composition = isOrphan ? [] : [testId];
});
[false, true].forEach(function (isEditRoot) {
var caseName = isEditRoot ?
"that are being edited" : "that are not being edited";
function itNavigatesAsExpected() {
if (isOrphan && !isEditRoot) {
it("navigates to the parent", function () {
expect(mockActionCapability.perform)
.toHaveBeenCalledWith('navigate');
});
} else {
it("does nothing", function () {
expect(mockActionCapability.perform)
.not.toHaveBeenCalled();
});
}
}
describe(caseName, function () {
beforeEach(function () {
mockEditor.isEditContextRoot.andReturn(isEditRoot);
});
describe("when navigation changes", function () {
beforeEach(function () {
mockNavigationService.addListener.mostRecentCall
.args[0](mockDomainObject);
});
itNavigatesAsExpected();
});
describe("when mutation occurs", function () {
beforeEach(function () {
mockMutationTopic.listen.mostRecentCall
.args[0](mockParentObject);
});
itNavigatesAsExpected();
});
});
});
});
});
});
});

View File

@@ -170,7 +170,7 @@ define([
"navigationService",
"$log"
],
"description": "Edit",
"description": "Edit this object.",
"category": "view-control",
"glyph": "p"
},

View File

@@ -50,24 +50,18 @@ define(
//If the object existed already, navigate to refresh view
// with previous object state.
if (domainObject.getModel().persisted) {
return domainObject.getCapability("action").perform("navigate");
domainObject.getCapability("action").perform("navigate");
} else {
//If the object was new, and user has cancelled, then
//navigate back to parent because nothing to show.
return domainObject.getCapability("location").getOriginal().then(function (original) {
domainObject.getCapability("location").getOriginal().then(function (original) {
parent = original.getCapability("context").getParent();
parent.getCapability("action").perform("navigate");
});
}
}
function cancel(allowed) {
return allowed && domainObject.getCapability("editor").cancel();
}
//Do navigation first in order to trigger unsaved changes dialog
return returnToBrowse()
.then(cancel);
return this.domainObject.getCapability("editor").cancel()
.then(returnToBrowse);
};
/**

View File

@@ -67,17 +67,10 @@ define(
}
function onCancel() {
if (self.domainObject.getModel().persisted !== undefined) {
//Fetch clean model from persistence
return self.persistenceCapability.refresh().then(function (result) {
self.persistPending = false;
return result;
});
} else {
return self.persistenceCapability.refresh().then(function (result) {
self.persistPending = false;
//Model is undefined in persistence, so return undefined.
return self.$q.when(undefined);
}
return result;
});
}
if (this.transactionService.isActive()) {

View File

@@ -24,11 +24,12 @@ define(
["../../src/actions/CancelAction"],
function (CancelAction) {
describe("The Cancel action", function () {
var mockDomainObject,
mockParentObject,
capabilities = {},
parentCapabilities = {},
//TODO: Disabled for NEM Beta
xdescribe("The Cancel action", function () {
var mockLocation,
mockDomainObject,
mockEditorCapability,
mockUrlService,
actionContext,
action;
@@ -41,114 +42,61 @@ define(
}
beforeEach(function () {
mockLocation = jasmine.createSpyObj(
"$location",
["path"]
);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[
"getCapability",
"hasCapability",
"getModel"
]
["getCapability", "hasCapability"]
);
mockDomainObject.getModel.andReturn({});
mockParentObject = jasmine.createSpyObj(
"parentObject",
[
"getCapability"
]
);
mockParentObject.getCapability.andCallFake(function (name) {
return parentCapabilities[name];
});
capabilities.editor = jasmine.createSpyObj(
mockEditorCapability = jasmine.createSpyObj(
"editor",
["save", "cancel", "isEditContextRoot"]
["save", "cancel"]
);
capabilities.action = jasmine.createSpyObj(
"actionCapability",
[
"perform"
]
);
capabilities.location = jasmine.createSpyObj(
"locationCapability",
[
"getOriginal"
]
);
capabilities.location.getOriginal.andReturn(mockPromise(mockDomainObject));
capabilities.context = jasmine.createSpyObj(
"contextCapability",
[
"getParent"
]
);
capabilities.context.getParent.andReturn(mockParentObject);
parentCapabilities.action = jasmine.createSpyObj(
"actionCapability",
[
"perform"
]
mockUrlService = jasmine.createSpyObj(
"urlService",
["urlForLocation"]
);
actionContext = {
domainObject: mockDomainObject
};
mockDomainObject.getCapability.andCallFake(function (name) {
return capabilities[name];
});
mockDomainObject.hasCapability.andReturn(true);
mockDomainObject.getCapability.andReturn(mockEditorCapability);
mockEditorCapability.cancel.andReturn(mockPromise(true));
mockDomainObject.hasCapability.andCallFake(function (name) {
return !!capabilities[name];
});
capabilities.editor.cancel.andReturn(mockPromise(true));
action = new CancelAction(actionContext);
action = new CancelAction(mockLocation, mockUrlService, actionContext);
});
it("only applies to domain object that is being edited", function () {
capabilities.editor.isEditContextRoot.andReturn(true);
it("only applies to domain object with an editor capability", function () {
expect(CancelAction.appliesTo(actionContext)).toBeTruthy();
expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor");
capabilities.editor.isEditContextRoot.andReturn(false);
expect(CancelAction.appliesTo(actionContext)).toBeFalsy();
mockDomainObject.hasCapability.andReturn(false);
mockDomainObject.getCapability.andReturn(undefined);
expect(CancelAction.appliesTo(actionContext)).toBeFalsy();
});
it("invokes the editor capability's cancel functionality when" +
" performed", function () {
mockDomainObject.getModel.andReturn({persisted: 1});
//Return true from navigate action
capabilities.action.perform.andReturn(mockPromise(true));
it("invokes the editor capability's save functionality when performed", function () {
// Verify precondition
expect(mockEditorCapability.cancel).not.toHaveBeenCalled();
action.perform();
// Should have called cancel
expect(capabilities.editor.cancel).toHaveBeenCalled();
expect(mockEditorCapability.cancel).toHaveBeenCalled();
// Definitely shouldn't call save!
expect(capabilities.editor.save).not.toHaveBeenCalled();
expect(mockEditorCapability.save).not.toHaveBeenCalled();
});
it("navigates to object if existing using navigate action", function () {
mockDomainObject.getModel.andReturn({persisted: 1});
//Return true from navigate action
capabilities.action.perform.andReturn(mockPromise(true));
it("returns to browse when performed", function () {
action.perform();
expect(capabilities.action.perform).toHaveBeenCalledWith("navigate");
});
it("navigates to parent if new using navigate action", function () {
mockDomainObject.getModel.andReturn({persisted: undefined});
action.perform();
expect(parentCapabilities.action.perform).toHaveBeenCalledWith("navigate");
expect(mockLocation.path).toHaveBeenCalledWith(
mockUrlService.urlForLocation("browse", mockDomainObject)
);
});
});
}

View File

@@ -57,15 +57,6 @@ define(
);
mockPersistence.persist.andReturn(fastPromise());
mockPersistence.refresh.andReturn(fastPromise());
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[
"getModel"
]
);
mockDomainObject.getModel.andReturn({persisted: 1});
capability = new TransactionalPersistenceCapability(mockQ, mockTransactionService, mockPersistence, mockDomainObject);
});
@@ -87,20 +78,6 @@ define(
expect(mockPersistence.refresh).toHaveBeenCalled();
});
it("if transaction is active, cancel call is queued that refreshes model when appropriate", function () {
mockTransactionService.isActive.andReturn(true);
capability.persist();
expect(mockTransactionService.addToTransaction).toHaveBeenCalled();
mockDomainObject.getModel.andReturn({});
mockTransactionService.addToTransaction.mostRecentCall.args[1]();
expect(mockPersistence.refresh).not.toHaveBeenCalled();
mockDomainObject.getModel.andReturn({persisted: 1});
mockTransactionService.addToTransaction.mostRecentCall.args[1]();
expect(mockPersistence.refresh).toHaveBeenCalled();
});
it("persist call is only added to transaction once", function () {
mockTransactionService.isActive.andReturn(true);
capability.persist();

View File

@@ -100,6 +100,5 @@
<glyph unicode="&#xe620;" glyph-name="icon-tabular-realtime" d="M896 960h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128zM448 668l25.060-25.32c7.916-7.922 18.856-12.822 30.94-12.822s23.024 4.9 30.94 12.822l75.5 76.3c29.97 30.338 71.571 49.128 117.56 49.128s87.59-18.79 117.544-49.112l50.456-50.997v-152.2c-24.111 8.83-44.678 22.255-61.542 39.342l-75.518 76.318c-7.916 7.922-18.856 12.822-30.94 12.822s-23.024-4.9-30.94-12.822l-75.5-76.3c-29.971-30.343-71.575-49.137-117.568-49.137-20.084 0-39.331 3.584-57.137 10.146l1.145 151.831zM320 0h-192c-35.26 0.214-63.786 28.74-64 63.98v128.020h256v-192zM320 256h-256v192h256v-192zM320 512h-256v192h256v-192zM640 0h-256v192h256v-192zM448 323.38v174.5c1.88-1.74 3.74-3.5 5.56-5.34l75.5-76.3c7.916-7.922 18.856-12.822 30.94-12.822s23.024 4.9 30.94 12.822l75.5 76.3c29.966 30.333 71.56 49.119 117.542 49.119 43.28 0 82.673-16.644 112.128-43.879l-0.11-174.399c-1.88 1.74-3.74 3.5-5.56 5.34l-75.5 76.3c-7.916 7.922-18.856 12.822-30.94 12.822s-23.024-4.9-30.94-12.822l-75.5-76.3c-29.966-30.333-71.56-49.119-117.542-49.119-43.28 0-82.673 16.644-112.128 43.879zM960 64c-0.214-35.26-28.74-63.786-63.98-64h-192.020v192h256v-128z" />
<glyph unicode="&#xe621;" glyph-name="icon-tabular-lad" d="M896 960h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128zM64 704h256v-192h-256v192zM64 448h256v-192h-256v192zM128 0c-35.26 0.214-63.786 28.74-64 63.98v128.020h256v-192h-192zM384 0v192h256v-192h-256zM960 64c-0.214-35.26-28.74-63.786-63.98-64h-192.020v192h256v-128zM960 448v-192h-576v192h64v64h-64v192h576v-192h-64v-64h64zM782.32 412.62l-110.32 55.16v172.22c0 17.673-14.327 32-32 32s-32-14.327-32-32v-211.78l145.68-72.84c4.172-2.133 9.1-3.383 14.32-3.383 17.675 0 32.003 14.328 32.003 32.003 0 12.454-7.114 23.247-17.501 28.536z" />
<glyph unicode="&#xe622;" glyph-name="icon-tabular-lad-set" d="M128 192v576c-70.606-0.215-127.785-57.394-128-127.979v-576.021c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979l-576 0.021c-70.606 0.215-127.785 57.394-128 127.979zM896 960h-576c-70.606-0.215-127.785-57.394-128-127.979v-576.021c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979v576.021c-0.215 70.606-57.394 127.785-127.979 128zM256 768h192v-128h-192v128zM256 576h192v-192h-192v192zM320 192c-35.26 0.214-63.786 28.74-64 63.98v64.020h192v-128h-128zM512 192v128h192v-128h-192zM960 256c-0.214-35.26-28.74-63.786-63.98-64h-128.020v128h192v-64zM960 384h-448v384h448v-384zM832 480c0.002 0 0.005 0 0.007 0 17.673 0 32 14.327 32 32 0 14.055-9.062 25.994-21.662 30.293l-74.345 24.767v104.94c0 17.673-14.327 32-32 32s-32-14.327-32-32v-151.060l117.88-39.3c3.018-1.040 6.495-1.64 10.113-1.64 0.003 0 0.005 0 0.008 0z" />
<glyph unicode="&#xe623;" glyph-name="icon-download" d="M832 384v-255.66l-0.34-0.34-639.66 0.34v255.66h-192v-256c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v256h-192zM512 320l448 448h-256v192h-384v-192h-256l448-448z" />
<glyph unicode="&#xe642;" glyph-name="icon-x" d="M384 448l-365.332-365.332c-24.89-24.89-24.89-65.62 0-90.51l37.49-37.49c24.89-24.89 65.62-24.89 90.51 0 0 0 365.332 365.332 365.332 365.332l365.332-365.332c24.89-24.89 65.62-24.89 90.51 0l37.49 37.49c24.89 24.89 24.89 65.62 0 90.51l-365.332 365.332c0 0 365.332 365.332 365.332 365.332 24.89 24.89 24.89 65.62 0 90.51l-37.49 37.49c-24.89 24.89-65.62 24.89-90.51 0 0 0-365.332-365.332-365.332-365.332l-365.332 365.332c-24.89 24.89-65.62 24.89-90.51 0l-37.49-37.49c-24.89-24.89-24.89-65.62 0-90.51 0 0 365.332-365.332 365.332-365.332z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -49,7 +49,7 @@ $uePaneMiniTabCollapsedW: 11px;
$ueEditLeftPaneW: 75%;
$treeSearchInputBarH: 25px;
$ueTimeControlH: (33px, 18px, 20px);
/*************** Panes */
// Panes
$ueBrowseLeftPaneTreeMinW: 150px;
$ueBrowseLeftPaneTreeMaxW: 35%;
$ueBrowseLeftPaneTreeW: 25%;
@@ -57,58 +57,48 @@ $ueBrowseRightPaneInspectMinW: 200px;
$ueBrowseRightPaneInspectMaxW: 35%;
$ueBrowseRightPaneInspectW: 20%;
$ueDesktopMinW: 600px;
/*************** Overlay */
// Overlay
$ovrTopBarH: 45px;
$ovrFooterH: 24px;
$overlayMargin: 25px;
/*************** Items */
// Items
$ueBrowseGridItemLg: 200px;
$ueBrowseGridItemTopBarH: 20px;
$ueBrowseGridItemBottomBarH: 30px;
$itemPadLR: 5px;
/*************** Tree */
// Tree
$treeVCW: 10px;
$treeTypeIconH: 1.4em; // was 16px
$treeTypeIconHPx: 16px;
$treeTypeIconW: 18px;
$treeContextTriggerW: 20px;
/*************** Tabular */
// Tabular
$tabularHeaderH: 22px; //18px
$tabularTdPadLR: $itemPadLR;
$tabularTdPadTB: 3px;
/*************** Imagery */
// Imagery
$imageMainControlBarH: 25px;
$imageThumbsD: 120px;
$imageThumbsWrapperH: $imageThumbsD * 1.4;
$imageThumbPad: 1px;
/*************** Ticks */
// Ticks
$ticksH: 25px;
$tickLblVMargin: 3px;
$tickLblH: 15px;
$tickLblW: 50px;
$tickH: $ticksH - $tickLblVMargin - $tickLblH;
$tickW: 1px;
/*************** Plots */
$plotYBarW: 60px;
$plotYLabelMinH: 20px;
$plotYLabelW: 10px;
$plotXBarH: 32px;
$plotLegendH: 20px;
$plotSwatchD: 8px;
// 1: Top, 2: right, 3: bottom, 4: left
$plotDisplayArea: ($plotLegendH + $interiorMargin, 0, $plotXBarH + $interiorMargin, $plotYBarW);
/* min plot height is based on user testing to find minimum useful height */
$plotMinH: 95px;
/*************** Bubbles */
// Bubbles
$bubbleArwSize: 10px;
$bubblePad: $interiorMargin;
$bubbleMinW: 100px;
$bubbleMaxW: 300px;
/*************** Forms */
// Forms
$reqSymbolW: 15px;
$reqSymbolM: $interiorMargin * 2;
$reqSymbolFontSize: 0.7em;
/*************** Wait Spinner Defaults */
// Wait Spinner Defaults
$waitSpinnerD: 32px;
$waitSpinnerTreeD: 20px;
$waitSpinnerBorderW: 5px;
@@ -134,8 +124,6 @@ $dirImgs: $dirCommonRes + 'images/';
/************************** TIMINGS */
$controlFadeMs: 100ms;
$browseToEditAnimMs: 400ms;
$editBorderPulseMs: 500ms;
/************************** LIMITS */
$glyphLimit: '\e603';

View File

@@ -39,20 +39,15 @@
@include pulse($animName: pulse-subtle, $dur: 500ms, $opacity0: 0.7);
}
@mixin animTo($animName, $propName, $propValStart, $propValEnd, $dur: 500ms, $delay: 0) {
@include keyframes($animName) {
from { #{propName}: $propValStart; }
to { #{$propName}: $propValEnd; }
@mixin pulseBorder($c: red, $dur: 500ms, $iteration: infinite, $delay: 0s, $opacity0: 0, $opacity100: 1) {
@include keyframes(pulseBorder) {
0% { border-color: rgba($c, $opacity0); }
100% { border-color: rgba($c, $opacity100); }
}
@include animToParams($animName, $dur: 500ms, $delay: 0)
}
@mixin animToParams($animName, $dur: 500ms, $delay: 0) {
@include animation-name($animName);
@include animation-name(pulseBorder);
@include animation-duration($dur);
@include animation-direction(alternate);
@include animation-iteration-count($iteration);
@include animation-timing-function(ease);
@include animation-delay($delay);
@include animation-fill-mode(both);
@include animation-direction(normal);
@include animation-iteration-count(1);
@include animation-timing-function(ease-in-out);
}
}

View File

@@ -348,6 +348,7 @@
display: inline-block;
font-family: 'symbolsfont';
margin-left: $interiorMarginSm;
vertical-align: top;
}
@mixin nice-textarea($bg: $colorBodyBg, $fg: $colorBodyFg) {

View File

@@ -36,7 +36,15 @@ $pad: $interiorMargin * $baseRatio;
padding: 0 $pad;
font-size: 0.7rem;
vertical-align: top;
@include btnSubtle($colorBtnBg, $colorBtnBgHov, $colorBtnFg, $colorBtnIcon);
.icon {
font-size: 0.8rem;
color: $colorKey;
}
.title-label {
vertical-align: top;
}
&.lg {
font-size: 1rem;
@@ -50,14 +58,19 @@ $pad: $interiorMargin * $baseRatio;
padding: 0 ($pad / $baseRatio) / 2;
}
&.major,
&.key-edit,
&.key-properties {
&.major {
$bg: $colorBtnMajorBg;
$hc: lighten($bg, 10%);
@include btnSubtle($bg, $hc, $colorBtnMajorFg, $colorBtnMajorFg);
}
&:not(.major) {
// bg, bgHov, fg, ic
@include btnSubtle($colorBtnBg, $colorBtnBgHov, $colorBtnFg, $colorBtnIcon);
}
&.pause-play {
}
&.t-save:before {
content:'\e612';
font-family: symbolsfont;
@@ -96,22 +109,6 @@ $pad: $interiorMargin * $baseRatio;
content: "\000039";
}
}
&.t-export {
&:before {
@extend .ui-symbol;
@extend .icon;
content: '\e623';
}
}
.icon {
font-size: 0.8rem;
}
.title-label {
vertical-align: top;
}
}
.s-icon-btn {
@@ -278,3 +275,4 @@ body.desktop .mini-tab-icon {
color: $colorPausedBg !important;
}
}

View File

@@ -1,10 +1,13 @@
.l-image-main-wrapper,
.l-image-main,
.l-image-main-controlbar,
.l-image-thumbs-wrapper {
@include absPosDefault(0, false);
}
/*************************************** MAIN LAYOUT */
.l-image-main-wrapper {
//@include test();
@if $enableImageryThumbs == true {
bottom: $interiorMargin*2 + $imageThumbsWrapperH;
}
@@ -12,14 +15,16 @@
min-width: 150px;
.l-image-main {
background-color: $colorPlotBg;
margin-bottom: $interiorMargin;
bottom: $imageMainControlBarH + $interiorMargin;
}
.l-image-main-controlbar {
&.l-flex-row { @include align-items(center); }
top: auto;
height: $imageMainControlBarH;
}
}
.l-image-thumbs-wrapper {
//@include test(red);
top: auto;
height: $imageThumbsWrapperH;
}
@@ -39,17 +44,24 @@
background-repeat: no-repeat;
}
.l-image-main {
//cursor: crosshair;
}
.l-image-main-controlbar {
//@include test();
font-size: 0.8em;
line-height: inherit;
line-height: $imageMainControlBarH;
.left, .right {
direction: rtl;
overflow: hidden;
}
.left {
//@include test(red);
text-align: left;
}
.right {
//@include test(green);
z-index: 2;
}
.l-date,
@@ -59,6 +71,7 @@
.l-mag {
direction: ltr;
display: inline-block;
//white-space: nowrap;
&:before {
content: "\000049";
}

View File

@@ -24,10 +24,6 @@
height: 100%;
}
.tabular-holder {
@include absPosDefault();
}
.tabular,
table {
box-sizing: border-box;
@@ -166,41 +162,4 @@ table {
min-width: 150px;
}
}
}
/********************************************************** SPECIFIC TABULAR VIEWS */
.tabular-holder {
&.t-exportable {
$btnExportH: 25px;
.l-view-section {
top: $btnExportH + $interiorMargin;
}
}
}
.child-frame {
.tabular-holder {
&.t-exportable {
$btnExportH: $btnFrameH;
.s-btn.t-export {
@include trans-prop-nice(opacity, $dur: 50ms);
opacity: 0;
}
.l-view-section {
@include trans-prop-nice(top, $dur: 150ms, $delay: 50ms);
top: 0;
}
&:hover {
.s-btn.t-export {
@include trans-prop-nice(opacity, 150ms, 100ms);
opacity: 1;
}
.l-view-section {
@include trans-prop-nice(top, $dur: 150ms);
top: $btnExportH + $interiorMargin;
}
}
}
}
}

View File

@@ -19,10 +19,12 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.abs.holder-plot {
// Fend off the scrollbar when less than min-height;
right: $interiorMargin;
}
$yBarW: 60px;
$yLabelW: 10px;
$xBarH: 32px;
$legendH: 20px;
$swatchD: 8px;
$plotDisplayArea: ($legendH + $interiorMargin, 0, $xBarH + $interiorMargin, $yBarW); // Top, right, bottom, left
.gl-plot {
color: $colorPlotFg;
@@ -30,7 +32,6 @@
position: relative;
width: 100%;
height: 100%;
min-height: $plotMinH;
.gl-plot-local-controls {
@include trans-prop-nice(opacity, 150ms);
@@ -53,17 +54,17 @@
top: auto;
right: 0;
bottom: $interiorMargin;
left: $plotYBarW;
height: $plotXBarH;
left: $yBarW;
height: $xBarH;
width: auto;
overflow: hidden;
}
&.gl-plot-y {
top: $plotLegendH + $interiorMargin;
top: $legendH + $interiorMargin;
right: auto;
bottom: nth($plotDisplayArea, 3);
left: 0;
width: $plotYBarW;
width: $yBarW;
}
}
@@ -145,7 +146,7 @@
@include transform(translateY(-50%));
min-width: 150px; // Need this due to enclosure of .select
top: 50%;
left: $plotYLabelW + $interiorMargin * 2;
left: $yLabelW + $interiorMargin * 2;
}
.t-plot-display-controls {
@@ -173,7 +174,7 @@
right: 0;
bottom: auto;
left: 0;
height: $plotLegendH;
height: $legendH;
overflow-x: hidden;
overflow-y: auto;
}
@@ -235,8 +236,8 @@
.color-swatch {
border-radius: 2px;
display: inline-block;
height: $plotSwatchD;
width: $plotSwatchD;
height: $swatchD;
width: $swatchD;
}
}
}
@@ -248,8 +249,8 @@
padding: 0px $itemPadLR;
.plot-color-swatch {
border: 1px solid $colorBodyBg;
height: $plotSwatchD + 1;
width: $plotSwatchD + 1;
height: $swatchD + 1;
width: $swatchD + 1;
}
}
}

View File

@@ -54,8 +54,7 @@
height: $ohH;
line-height: $ohH;
padding: 0 $interiorMargin;
> span,
&:before {
> span {
font-size: 0.65rem;
}
}

View File

@@ -237,10 +237,30 @@ body.desktop .pane .mini-tab-icon.toggle-pane {
top: $ueTopBarH + $interiorMarginLg;
}
.l-object-wrapper {
@extend .abs;
.object-holder-main {
@extend .abs;
}
.l-edit-controls {
//@include trans-prop-nice((opacity, height), 0.25s);
border-bottom: 1px solid $colorInteriorBorder;
line-height: $ueEditToolBarH;
height: 0px;
opacity: 0;
.tool-bar {
right: $interiorMargin;
}
}
}
.l-object-wrapper-inner {
@include trans-prop-nice-resize(0.25s);
}
.object-browse-bar .s-btn,
.top-bar .buttons-main .s-btn,
.top-bar .s-menu-btn,
@@ -268,9 +288,8 @@ body.desktop .pane .mini-tab-icon.toggle-pane {
.left {
padding-right: $interiorMarginLg;
.l-back {
.l-back:not(.s-status-editing) {
margin-right: $interiorMarginLg;
&.s-status-editing { display: none; }
}
}
}
@@ -357,49 +376,19 @@ body.desktop {
.s-status-editing {
.l-object-wrapper {
$t2Dur: $browseToEditAnimMs;
$t1Dur: $t2Dur / 2;
$pulseDur: $editBorderPulseMs;
$bC0: rgba($colorEditAreaFg, 0.5);
$bC100: rgba($colorEditAreaFg, 1);
background-color: $colorEditAreaBg;
@include pulseBorder($colorEditAreaFg, $dur: 1s, $opacity0: 0.3);
border-radius: $controlCr;
border: 1px dotted $bC0;
// Transition 1
@include keyframes(wrapperIn) {
from { border: 0px dotted transparent; padding: 0; }
to { border: 1px dotted $bC0; padding: 5px; }
background-color: $colorEditAreaBg;
border-color: $colorEditAreaFg;
border-width: 2px;
border-style: dotted;
.l-object-wrapper-inner {
@include absPosDefault(3px, hidden);
}
// Do last
@include keyframes(pulseNew) {
from { border-color: $bC0; }
to { border-color: $bC100; }
}
@include animation-name(wrapperIn, pulseNew);
@include animation-duration($t1Dur, $pulseDur);
@include animation-delay(0s, $t1Dur + $t2Dur);
@include animation-direction(normal, alternate);
@include animation-fill-mode(both, none);
@include animation-iteration-count(1, infinite);
@include animation-timing-function(ease-in-out, linear);
.l-edit-controls {
height: 0;
border-bottom: 1px solid $colorInteriorBorder;
// Transition 2: reveal edit controls
@include keyframes(editIn) {
from { border-bottom: 0px solid transparent; height: 0; margin-bottom: 0; }
to { border-bottom: 1px solid $colorInteriorBorder; height: $ueEditToolBarH + $interiorMargin; margin-bottom: $interiorMargin; }
}
@include animToParams(editIn, $dur: $t2Dur, $delay: $t1Dur);
.tool-bar {
right: $interiorMargin;
}
height: $ueEditToolBarH + $interiorMargin;
margin-bottom: $interiorMargin;
opacity: 1;
}
}
}

View File

@@ -29,12 +29,10 @@
!structure.validate(ngModel[field])),
'picker-icon': structure.format === 'utc' || !structure.format
}">
</input>
<a class="ui-symbol icon icon-calendar"
ng-if="!picker.active && (structure.format === 'utc' || !structure.format)"
ng-click="picker.active = !picker.active"></a>
<!-- If picker active show icon with no onclick to prevent double registration of clicks -->
<a class="ui-symbol icon icon-calendar" ng-if="picker.active"></a>
</input><a class="ui-symbol icon icon-calendar"
ng-if="structure.format === 'utc' || !structure.format"
ng-click="picker.active = !picker.active">
</a>
<mct-popup ng-if="picker.active">
<div mct-click-elsewhere="picker.active = false">
<mct-control key="'datetime-picker'"

View File

@@ -72,17 +72,6 @@ define(
if ($scope.ngBlur) {
$scope.ngBlur();
}
// If picker is active, dismiss it when valid value has been selected
// This 'if' is to avoid unnecessary validation if picker is not active
if ($scope.picker.active) {
if ($scope.structure.validate && $scope.structure.validate($scope.ngModel[$scope.field])) {
$scope.picker.active = false;
} else if (!$scope.structure.validate) {
//If picker visible, but no validation function, hide picker
$scope.picker.active = false;
}
}
}
}
@@ -104,6 +93,7 @@ define(
$scope.$watch('ngModel[field]', updateFromModel);
$scope.$watch('pickerModel.value', updateFromPicker);
$scope.$watch('textValue', updateFromView);
}
return DateTimeFieldController;

View File

@@ -51,9 +51,7 @@ define(
yMax = yMin + rect.height;
if (x < xMin || x > xMax || y < yMin || y > yMax) {
scope.$apply(function () {
scope.$eval(attrs.mctClickElsewhere);
});
scope.$eval(attrs.mctClickElsewhere);
}
}

View File

@@ -104,8 +104,6 @@ define(
});
it("triggers an evaluation of its related Angular expression", function () {
expect(mockScope.$apply).toHaveBeenCalled();
mockScope.$apply.mostRecentCall.args[0]();
expect(mockScope.$eval)
.toHaveBeenCalledWith(testAttrs.mctClickElsewhere);
});

View File

@@ -14,7 +14,7 @@ $colorAHov: #fff;
$contrastRatioPercent: 7%;
$hoverRatioPercent: 10%;
$basicCr: 3px;
$controlCr: 2px;
$controlCr: 3px;
$smallCr: 2px;
// Buttons and Controls
@@ -183,13 +183,12 @@ $scrollbarThumbColorOverlay: lighten($colorOvrBg, 10%);
$scrollbarThumbColorOverlayHov: lighten($scrollbarThumbColorOverlay, 2%);
// Splitter
$splitterD: 17px; // splitterD and $splitterHandleD should both be odd, or even
$splitterD: 25px; // splitterD and HandleD should both be odd, or even
$splitterHandleD: 1px;
$splitterDSm: 17px; // Smaller splitter, used inside elements like a Timeline view
$colorSplitterBg: rgba(#fff, 0.1); //pullForward($colorBodyBg, 5%);
$splitterShdw: rgba(black, 0.4) 0 0 3px;
$splitterEndCr: none;
$colorSplitterHover: pullForward($colorBodyBg, 40%);
$colorSplitterHover: pullForward($colorBodyBg, 15%);
$colorSplitterActive: $colorKey;
// Mobile

View File

@@ -183,12 +183,12 @@ $scrollbarThumbColorOverlay: darken($colorOvrBg, 50%);
$scrollbarThumbColorOverlayHov: $scrollbarThumbColorHov;
// Splitter
$splitterD: 16px; // splitterD and $splitterHandleD should both be odd, or even
$splitterD: 24px;
$splitterHandleD: 2px;
$colorSplitterBg: pullForward($colorBodyBg, 10%);
$splitterShdw: none;
$splitterEndCr: none;
$colorSplitterHover: pullForward($colorBodyBg, 30%);
$colorSplitterHover: none;
$colorSplitterActive: $colorKey;
// Mobile

View File

@@ -60,6 +60,11 @@ define(
this.$q = $q;
}
function getKey(id) {
var parts = id.split(":");
return parts.length > 1 ? parts.slice(1).join(":") : id;
}
/**
* Checks if the value returned is falsey, and if so returns a
* rejected promise
@@ -126,7 +131,7 @@ define(
// ...and persist
return persistenceFn.apply(persistenceService, [
this.getSpace(),
this.getKey(),
getKey(domainObject.getId()),
domainObject.getModel()
]).then(function (result) {
return rejectIfFalsey(result, self.$q);
@@ -154,7 +159,7 @@ define(
return this.persistenceService.readObject(
this.getSpace(),
this.getKey()
this.domainObject.getId()
).then(updateModel);
};
@@ -173,17 +178,6 @@ define(
return this.identifierService.parse(id).getSpace();
};
/**
* Get the key for this domain object in the given space.
*
* @returns {string} the key of the object in it's space.
*/
PersistenceCapability.prototype.getKey = function () {
var id = this.domainObject.getId();
return this.identifierService.parse(id).getKey();
};
return PersistenceCapability;
}
);

View File

@@ -35,8 +35,7 @@ define(
mockNofificationService,
mockCacheService,
mockQ,
key = "persistence key",
id = "object identifier",
id = "object id",
model,
SPACE = "some space",
persistence,
@@ -102,7 +101,6 @@ define(
});
mockIdentifierService.parse.andReturn(mockIdentifier);
mockIdentifier.getSpace.andReturn(SPACE);
mockIdentifier.getKey.andReturn(key);
persistence = new PersistenceCapability(
mockCacheService,
mockPersistenceService,
@@ -126,7 +124,7 @@ define(
expect(mockPersistenceService.createObject).toHaveBeenCalledWith(
SPACE,
key,
id,
model
);
});
@@ -140,7 +138,7 @@ define(
expect(mockPersistenceService.updateObject).toHaveBeenCalledWith(
SPACE,
key,
id,
model
);
});

View File

@@ -82,6 +82,16 @@ define(
expect(result.a.getModel()).toEqual(model);
});
//TODO: Disabled for NEM Beta
xit("provides a new, fully constituted domain object for a" +
" provided model", function () {
var model = { someKey: "some value"},
result;
result = provider.newObject("a", model);
expect(result.getId()).toEqual("a");
expect(result.getModel()).toEqual(model);
});
});
}
);

View File

@@ -81,7 +81,6 @@ define(
if (phase.toLowerCase() === 'preparing' && !this.dialog) {
this.dialog = this.dialogService.showBlockingMessage({
title: "Preparing to copy objects",
hint: "Do not navigate away from this page or close this browser tab while this message is displayed.",
unknownProgress: true,
severity: "info"
});

View File

@@ -24,7 +24,10 @@ define(
[],
function () {
var DISALLOWED_ACTIONS = ["move"];
var DISALLOWED_ACTIONS = [
"move",
"copy"
];
/**
* This policy prevents performing move/copy/link actions across

View File

@@ -70,25 +70,27 @@ define(
policy = new CrossSpacePolicy();
});
describe("for move actions", function () {
beforeEach(function () {
testActionMetadata.key = 'move';
});
['move', 'copy'].forEach(function (key) {
describe("for " + key + " actions", function () {
beforeEach(function () {
testActionMetadata.key = key;
});
it("allows same-space changes", function () {
expect(policy.allow(mockAction, sameSpaceContext))
.toBe(true);
});
it("allows same-space changes", function () {
expect(policy.allow(mockAction, sameSpaceContext))
.toBe(true);
});
it("disallows cross-space changes", function () {
expect(policy.allow(mockAction, crossSpaceContext))
.toBe(false);
});
it("disallows cross-space changes", function () {
expect(policy.allow(mockAction, crossSpaceContext))
.toBe(false);
});
it("allows actions with no selectedObject", function () {
expect(policy.allow(mockAction, {
domainObject: makeObject('a')
})).toBe(true);
it("allows actions with no selectedObject", function () {
expect(policy.allow(mockAction, {
domainObject: makeObject('a')
})).toBe(true);
});
});
});

View File

@@ -42,7 +42,7 @@ define(
'parent'
];
describe("ConductorRepresenter", function () {
xdescribe("ConductorRepresenter", function () {
var mockThrottle,
mockConductorService,
mockCompile,

View File

@@ -1,18 +1,23 @@
<div class="t-imagery" ng-controller="ImageryController as imagery">
<div class="l-image-main-wrapper l-flex-col"
<div
class="l-image-main-wrapper"
ng-mouseenter="showLocalControls = true;"
ng-mouseleave="showLocalControls = false;">
ng-mouseleave="showLocalControls = false;"
>
<div
class="l-local-controls s-local-controls"
ng-show="false && showLocalControls">
<a class="s-btn"
ng-show="false && showLocalControls"
>
<a
class="s-btn"
ng-click="plot.stepBackPanZoom()"
ng-show="1"
title="Restore previous pan/zoom">
<span class="ui-symbol icon">&lt;</span>
</a>
<a class="s-btn"
<a
class="s-btn"
ng-click="plot.unzoom()"
ng-show="1"
title="Reset pan/zoom">
@@ -20,23 +25,29 @@
</a>
</div>
<div class="l-image-main s-image-main flex-elem grows"
<div
class="l-image-main s-image-main"
ng-class="{ paused: imagery.paused(), stale:false }"
mct-background-image="imagery.getImageUrl()">
mct-background-image="imagery.getImageUrl()"
>
</div>
<div class="l-image-main-controlbar flex-elem l-flex-row">
<div class="l-image-main-controlbar l-flex-row">
<div class="left flex-elem grows">
<a class="s-btn show-thumbs sm hidden"
ng-click="showThumbsBubble = (showThumbsBubble)? false:true"><span class="ui-symbol icon"></span></a>
<a
class="s-btn show-thumbs sm hidden"
ng-click="showThumbsBubble = (showThumbsBubble)? false:true"
><span class="ui-symbol icon"></span></a>
<span class="l-timezone">{{imagery.getZone()}}</span>
<span class="l-time">{{imagery.getTime()}}</span>
<span class="l-date">{{imagery.getDate()}}</span>
</div>
<div class="right flex-elem">
<a class="s-btn pause-play"
<a
class="s-btn pause-play"
ng-click="imagery.paused(!imagery.paused())"
ng-class="{ paused: imagery.paused() }"><span class="ui-symbol icon"></span></a>
ng-class="{ paused: imagery.paused() }"
><span class="ui-symbol icon"></span></a>
<a href="{{imagery.getImageUrl()}}"
ng-if="imagery.getImageUrl()"
target="_blank"
@@ -47,7 +58,8 @@
class="s-btn l-mag s-mag ui-symbol vsm"
ng-click="clipped = false"
ng-show="clipped === true"
title="Not all of image is visible; click to reset."></a>
title="Not all of image is visible; click to reset."
></a>
</div>
</div>
</div>

View File

@@ -109,7 +109,7 @@ define([
{
"key": "HistoricalTableController",
"implementation": HistoricalTableController,
"depends": ["$scope", "telemetryHandler", "telemetryFormatter", "$timeout"]
"depends": ["$scope", "telemetryHandler", "telemetryFormatter"]
},
{
"key": "RealtimeTableController",

View File

@@ -1,9 +1,8 @@
<div ng-controller="HistoricalTableController" ng-class="{'loading': loading}">
<div ng-controller="HistoricalTableController">
<mct-table
headers="headers"
rows="rows"
enableFilter="true"
enableSort="true"
class="tabular-holder t-exportable">
enableSort="true">
</mct-table>
</div>

View File

@@ -1,9 +1,4 @@
<a class="t-btn l-btn s-btn t-export"
ng-click="exportAsCSV()"
title="Export This View's Data">
Export
</a>
<div class="l-view-section scrolling" style="overflow: auto;" mct-resize="resize()">
<div class="l-view-section scrolling" style="overflow: auto;">
<table class="sizing-table">
<tbody>
<tr>

View File

@@ -4,7 +4,6 @@
rows="rows"
enableFilter="true"
enableSort="true"
class="tabular-holder t-exportable"
auto-scroll="true">
auto-scroll="autoScroll">
</mct-table>
</div>

View File

@@ -25,7 +25,6 @@ define(
'./TelemetryTableController'
],
function (TableController) {
var BATCH_SIZE = 1000;
/**
* Extends TelemetryTableController and adds real-time streaming
@@ -36,82 +35,32 @@ define(
* @param telemetryFormatter
* @constructor
*/
function HistoricalTableController($scope, telemetryHandler, telemetryFormatter, $timeout) {
var self = this;
this.$timeout = $timeout;
this.timeoutHandle = undefined;
this.batchSize = BATCH_SIZE;
$scope.$on("$destroy", function () {
if (self.timeoutHandle) {
self.$timeout.cancel(self.timeoutHandle);
}
});
function HistoricalTableController($scope, telemetryHandler, telemetryFormatter) {
TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
}
HistoricalTableController.prototype = Object.create(TableController.prototype);
/**
* Set provided row data on scope, and cancel loading spinner
* @private
* Populates historical data on scope when it becomes available from
* the telemetry API
*/
HistoricalTableController.prototype.doneProcessing = function (rowData) {
this.$scope.rows = rowData;
this.$scope.loading = false;
};
/**
* Processes an array of objects, formatting the telemetry available
* for them and setting it on scope when done
* @private
*/
HistoricalTableController.prototype.processTelemetryObjects = function (objects, offset, start, rowData) {
var telemetryObject = objects[offset],
series,
i = start,
pointCount,
end;
//No more objects to process
if (!telemetryObject) {
return this.doneProcessing(rowData);
}
series = this.handle.getSeries(telemetryObject);
pointCount = series.getPointCount();
end = Math.min(start + this.batchSize, pointCount);
//Process rows in a batch with size not exceeding a maximum length
for (; i < end; i++) {
rowData.push(this.table.getRowValues(telemetryObject,
this.handle.makeDatum(telemetryObject, series, i)));
}
//Done processing all rows for this object.
if (end >= pointCount) {
offset++;
end = 0;
}
// Done processing either a batch or an object, yield process
// before continuing processing
this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, objects, offset, end, rowData));
};
/**
* Populates historical data on scope when it becomes available from
* the telemetry API
*/
HistoricalTableController.prototype.addHistoricalData = function () {
if (this.timeoutHandle) {
this.$timeout.cancel(this.timeoutHandle);
}
var rowData = [],
self = this;
this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, this.handle.getTelemetryObjects(), 0, 0, []));
this.handle.getTelemetryObjects().forEach(function (telemetryObject) {
var series = self.handle.getSeries(telemetryObject) || {},
pointCount = series.getPointCount ? series.getPointCount() : 0,
i = 0;
for (; i < pointCount; i++) {
rowData.push(self.table.getRowValues(telemetryObject,
self.handle.makeDatum(telemetryObject, series, i)));
}
});
this.$scope.rows = rowData;
};
return HistoricalTableController;

View File

@@ -12,7 +12,7 @@ define(
* @param element
* @constructor
*/
function MCTTableController($scope, $timeout, element, exportService) {
function MCTTableController($scope, $timeout, element) {
var self = this;
this.$scope = $scope;
@@ -46,16 +46,6 @@ define(
setDefaults($scope);
$scope.exportAsCSV = function () {
var headers = $scope.displayHeaders;
exportService.exportCSV($scope.displayRows.map(function (row) {
return headers.reduce(function (r, header) {
r[header] = row[header].text;
return r;
}, {});
}), { headers: headers });
};
$scope.toggleSort = function (key) {
if (!$scope.enableSort) {
return;
@@ -86,12 +76,6 @@ define(
*/
$scope.$on('add:row', this.addRow.bind(this));
$scope.$on('remove:row', this.removeRow.bind(this));
/*
* Listen for resize events to trigger recalculation of table width
*/
$scope.resize = this.setElementSizes.bind(this);
}
/**

View File

@@ -38,7 +38,30 @@ define(
function RealtimeTableController($scope, telemetryHandler, telemetryFormatter) {
TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
$scope.autoScroll = false;
this.maxRows = 100000;
/*
* Determine if auto-scroll should be enabled. Is enabled
* automatically when telemetry type is string
*/
function hasStringTelemetry(domainObject) {
var telemetry = domainObject &&
domainObject.getCapability('telemetry'),
metadata = telemetry ? telemetry.getMetadata() : {},
ranges = metadata.ranges || [];
return ranges.some(function (range) {
return range.format === 'string';
});
}
$scope.$watch('domainObject', function (domainObject) {
//When a domain object becomes available, check whether the
// view should auto-scroll to the bottom.
if (domainObject && hasStringTelemetry(domainObject)) {
$scope.autoScroll = true;
}
});
}
RealtimeTableController.prototype = Object.create(TableController.prototype);
@@ -68,7 +91,6 @@ define(
self.$scope.rows.length - 1);
}
});
this.$scope.loading = false;
};
return RealtimeTableController;

View File

@@ -72,10 +72,10 @@ define(
* Maintain a configuration object on scope that stores column
* configuration. On change, synchronize with object model.
*/
$scope.$watchCollection('configuration.table.columns', function (newColumns, oldColumns) {
if (newColumns !== oldColumns) {
$scope.$watchCollection('configuration.table.columns', function (columns) {
if (columns) {
self.domainObject.useCapability('mutation', function (model) {
model.configuration.table.columns = newColumns;
model.configuration.table.columns = columns;
});
self.domainObject.getCapability('persistence').persist();
}

View File

@@ -83,24 +83,16 @@ define(
* @private
*/
TelemetryTableController.prototype.registerChangeListeners = function () {
var self = this;
this.unregisterChangeListeners();
// When composition changes, re-subscribe to the various
// telemetry subscriptions
this.changeListeners.push(this.$scope.$watchCollection(
'domainObject.getModel().composition',
function (newVal, oldVal) {
if (newVal !== oldVal) {
self.subscribe();
}
})
);
'domainObject.getModel().composition', this.subscribe.bind(this)));
//Change of bounds in time conductor
this.changeListeners.push(this.$scope.$on('telemetry:display:bounds',
this.subscribe.bind(this))
);
this.subscribe.bind(this)));
};
/**
@@ -140,7 +132,6 @@ define(
if (this.handle) {
this.handle.unsubscribe();
}
this.$scope.loading = true;
this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
this.$scope.domainObject,

View File

@@ -81,13 +81,7 @@ define(
return {
restrict: "E",
template: TableTemplate,
controller: [
'$scope',
'$timeout',
'$element',
'exportService',
MCTTableController
],
controller: ['$scope', '$timeout', '$element', MCTTableController],
scope: {
headers: "=",
rows: "=",

View File

@@ -30,21 +30,23 @@ define(
var TEST_DOMAIN_VALUE = "some formatted domain value";
describe("A domain column", function () {
var mockDatum,
var mockDataSet,
testMetadata,
mockFormatter,
column;
beforeEach(function () {
mockDataSet = jasmine.createSpyObj(
"data",
["getDomainValue"]
);
mockFormatter = jasmine.createSpyObj(
"formatter",
["formatDomainValue", "formatRangeValue"]
);
testMetadata = {
key: "testKey",
name: "Test Name",
format: "Test Format"
name: "Test Name"
};
mockFormatter.formatDomainValue.andReturn(TEST_DOMAIN_VALUE);
@@ -55,24 +57,24 @@ define(
expect(column.getTitle()).toEqual("Test Name");
});
describe("when given a datum", function () {
beforeEach(function () {
mockDatum = {
testKey: "testKeyValue"
};
});
xit("looks up data from a data set", function () {
column.getValue(undefined, mockDataSet, 42);
expect(mockDataSet.getDomainValue)
.toHaveBeenCalledWith(42, "testKey");
});
it("looks up data from the given datum", function () {
expect(column.getValue(undefined, mockDatum))
.toEqual({ text: TEST_DOMAIN_VALUE });
});
xit("formats domain values as time", function () {
mockDataSet.getDomainValue.andReturn(402513731000);
it("uses formatter to format domain values as requested", function () {
column.getValue(undefined, mockDatum);
expect(mockFormatter.formatDomainValue)
.toHaveBeenCalledWith("testKeyValue", "Test Format");
});
// Should have just given the value the formatter gave
expect(column.getValue(undefined, mockDataSet, 42).text)
.toEqual(TEST_DOMAIN_VALUE);
// Make sure that service interactions were as expected
expect(mockFormatter.formatDomainValue)
.toHaveBeenCalledWith(402513731000);
expect(mockFormatter.formatRangeValue)
.not.toHaveBeenCalled();
});
});

View File

@@ -34,8 +34,6 @@ define(
mockDomainObject,
mockTable,
mockConfiguration,
mockAngularTimeout,
mockTimeoutHandle,
watches,
controller;
@@ -65,11 +63,6 @@ define(
watches[expression] = callback;
});
mockTimeoutHandle = jasmine.createSpy("timeoutHandle");
mockAngularTimeout = jasmine.createSpy("$timeout");
mockAngularTimeout.andReturn(mockTimeoutHandle);
mockAngularTimeout.cancel = jasmine.createSpy("cancelTimeout");
mockConfiguration = {
'range1': true,
'range2': true,
@@ -114,7 +107,7 @@ define(
]);
mockTelemetryHandler.handle.andReturn(mockTelemetryHandle);
controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter, mockAngularTimeout);
controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter);
controller.table = mockTable;
controller.handle = mockTelemetryHandle;
});
@@ -170,13 +163,6 @@ define(
controller.addHistoricalData(mockDomainObject, mockSeries);
// Angular timeout is called a minumum of twice, regardless
// of batch size used.
expect(mockAngularTimeout).toHaveBeenCalled();
mockAngularTimeout.mostRecentCall.args[0]();
expect(mockAngularTimeout.calls.length).toEqual(2);
mockAngularTimeout.mostRecentCall.args[0]();
expect(controller.$scope.rows.length).toBe(5);
expect(controller.$scope.rows[0]).toBe(mockRow);
});
@@ -212,7 +198,7 @@ define(
' object composition changes', function () {
controller.registerChangeListeners();
expect(watches['domainObject.getModel().composition']).toBeDefined();
watches['domainObject.getModel().composition']([], []);
watches['domainObject.getModel().composition']();
expect(controller.subscribe).toHaveBeenCalled();
});
@@ -233,78 +219,6 @@ define(
});
});
describe('Yields thread', function () {
var mockSeries,
mockRow;
beforeEach(function () {
mockSeries = {
getPointCount: function () {
return 5;
},
getDomainValue: function () {
return 'Domain Value';
},
getRangeValue: function () {
return 'Range Value';
}
};
mockRow = {'domain': 'Domain Value', 'range': 'Range Value'};
mockTelemetryHandle.makeDatum.andCallFake(function () {
return mockRow;
});
mockTable.getRowValues.andReturn(mockRow);
mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]);
mockTelemetryHandle.getSeries.andReturn(mockSeries);
});
it('when row count exceeds batch size', function () {
controller.batchSize = 3;
controller.addHistoricalData(mockDomainObject, mockSeries);
//Timeout is called a minimum of two times
expect(mockAngularTimeout).toHaveBeenCalled();
mockAngularTimeout.mostRecentCall.args[0]();
expect(mockAngularTimeout.calls.length).toEqual(2);
mockAngularTimeout.mostRecentCall.args[0]();
//Because it yields, timeout will have been called a
// third time for the batch.
expect(mockAngularTimeout.calls.length).toEqual(3);
mockAngularTimeout.mostRecentCall.args[0]();
expect(controller.$scope.rows.length).toBe(5);
expect(controller.$scope.rows[0]).toBe(mockRow);
});
it('cancelling any outstanding timeouts', function () {
controller.batchSize = 3;
controller.addHistoricalData(mockDomainObject, mockSeries);
expect(mockAngularTimeout).toHaveBeenCalled();
mockAngularTimeout.mostRecentCall.args[0]();
controller.addHistoricalData(mockDomainObject, mockSeries);
expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle);
});
it('cancels timeout on scope destruction', function () {
controller.batchSize = 3;
controller.addHistoricalData(mockDomainObject, mockSeries);
//Destroy is used by parent class as well, so multiple
// calls are made to scope.$on
var destroyCalls = mockScope.$on.calls.filter(function (call) {
return call.args[0] === '$destroy';
});
//Call destroy function
expect(destroyCalls.length).toEqual(2);
destroyCalls[0].args[1]();
expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle);
});
});
});
}
);

View File

@@ -32,8 +32,7 @@ define(
mockScope,
watches,
mockTimeout,
mockElement,
mockExportService;
mockElement;
function promise(value) {
return {
@@ -68,20 +67,11 @@ define(
offsetHeight: 1000
};
mockExportService = jasmine.createSpyObj('exportService', [
'exportCSV'
]);
mockScope.displayHeaders = true;
mockTimeout = jasmine.createSpy('$timeout');
mockTimeout.andReturn(promise(undefined));
controller = new MCTTableController(
mockScope,
mockTimeout,
mockElement,
mockExportService
);
controller = new MCTTableController(mockScope, mockTimeout, mockElement);
spyOn(controller, 'setVisibleRows').andCallThrough();
});
@@ -159,22 +149,6 @@ define(
expect(controller.setVisibleRows).toHaveBeenCalled();
});
it("can be exported as CSV", function () {
controller.setRows(testRows);
controller.setHeaders(Object.keys(testRows[0]));
mockScope.exportAsCSV();
expect(mockExportService.exportCSV)
.toHaveBeenCalled();
mockExportService.exportCSV.mostRecentCall.args[0]
.forEach(function (row, i) {
Object.keys(row).forEach(function (k) {
expect(row[k]).toEqual(
mockScope.displayRows[i][k].text
);
});
});
});
describe('sorting', function () {
var sortedRows;

View File

@@ -155,6 +155,13 @@ define(
expect(mockScope.rows[0].row).toBe(1);
});
});
it('enables autoscroll for event telemetry', function () {
controller.subscribe();
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockScope.autoScroll).toBe(true);
});
});
}
);

View File

@@ -91,12 +91,7 @@ define([
"name": "Export Timeline as CSV",
"category": "contextual",
"implementation": ExportTimelineAsCSVAction,
"depends": [
"$log",
"exportService",
"notificationService",
"resources[]"
]
"depends": ["exportService", "notificationService"]
}
],
"constants": [
@@ -472,7 +467,6 @@ define([
"implementation": TimelineZoomController,
"depends": [
"$scope",
"$window",
"TIMELINE_ZOOM_CONFIGURATION"
]
},

View File

@@ -23,13 +23,6 @@
.l-timeline-holder {
@include absPosDefault();
&.split-layout {
>.splitter {
// Top of splitter within Timelines should be 0
top: 0;
}
}
.l-header {
@include user-select(none);
cursor: default;
@@ -65,7 +58,7 @@
}
&.l-tabular-r {
// Start, end, duration, activity modes columns
@include scrollH(scroll);
@include scrollH();
left: $timelineTabularTitleW;
.l-width {
@include absPosDefault(0, visible);
@@ -311,6 +304,11 @@
}
}
.splitter {
// Top of splitter within Timelines should be 0
top: 0;
}
// Ticks
.l-ticks,
.l-subticks {
@@ -333,4 +331,4 @@
&:hover {
background-color: $colorItemTreeHoverBg;
}
}
}

View File

@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<div class="t-timeline-gantt l-timeline-gantt s-timeline-gantt"
ng-class="timespan ? { sm: gantt.width(timespan, parameters.scroll, parameters.toPixels) < 25 } : {}"
ng-class="{ sm: gantt.width(timespan, parameters.scroll, parameters.toPixels) < 25 }"
title="{{model.name}}"
ng-controller="TimelineGanttController as gantt"
ng-style="timespan ? {

View File

@@ -128,7 +128,7 @@
<div style="overflow: hidden; position: absolute; left: 0; top: 0; right: 0; height: 30px;" mct-scroll-x="scroll.x">
<mct-include key="'timeline-ticks'"
parameters="{
fullWidth: zoomController.width(timelineController.end()),
fullWidth: timelineController.width(zoomController),
start: scroll.x,
width: scroll.width,
step: zoomController.toPixels(zoomController.zoom()),
@@ -141,7 +141,7 @@
mct-scroll-x="scroll.x"
mct-scroll-y="scroll.y">
<div class="l-width-control"
ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
ng-style="{ width: timelineController.width(zoomController) + 'px' }">
<div class="t-swimlane s-swimlane l-swimlane"
ng-repeat="swimlane in timelineController.swimlanes()"
ng-class="{
@@ -197,7 +197,7 @@
<div mct-scroll-x="scroll.x"
class="t-pane-r-scroll-h-control l-scroll-control s-scroll-control">
<div class="l-width-control"
ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
ng-style="{ width: timelineController.width(zoomController) + 'px' }">
</div>
</div>
</div>

View File

@@ -27,15 +27,11 @@ define([], function () {
* in a domain object's composition.
* @param {number} index the zero-based index of the composition
* element associated with this column
* @param idMap an object containing key value pairs, where keys
* are domain object identifiers and values are whatever
* should appear in CSV output in their place
* @constructor
* @implements {platform/features/timeline.TimelineCSVColumn}
*/
function CompositionColumn(index, idMap) {
function CompositionColumn(index) {
this.index = index;
this.idMap = idMap;
}
CompositionColumn.prototype.name = function () {
@@ -45,9 +41,7 @@ define([], function () {
CompositionColumn.prototype.value = function (domainObject) {
var model = domainObject.getModel(),
composition = model.composition || [];
return composition.length > this.index ?
this.idMap[composition[this.index]] : "";
return (composition[this.index]) || "";
};
return CompositionColumn;

View File

@@ -27,23 +27,14 @@ define(["./ExportTimelineAsCSVTask"], function (ExportTimelineAsCSVTask) {
*
* @param exportService the service used to perform the CSV export
* @param notificationService the service used to show notifications
* @param {Array} resources an array of `resources` extensions
* @param context the Action's context
* @implements {Action}
* @constructor
* @memberof {platform/features/timeline}
*/
function ExportTimelineAsCSVAction(
$log,
exportService,
notificationService,
resources,
context
) {
this.$log = $log;
function ExportTimelineAsCSVAction(exportService, notificationService, context) {
this.task = new ExportTimelineAsCSVTask(
exportService,
resources,
context.domainObject
);
this.notificationService = notificationService;
@@ -54,15 +45,13 @@ define(["./ExportTimelineAsCSVTask"], function (ExportTimelineAsCSVTask) {
notification = notificationService.notify({
title: "Exporting CSV",
unknownProgress: true
}),
$log = this.$log;
});
return this.task.run()
.then(function () {
notification.dismiss();
})
.catch(function (err) {
$log.warn(err);
.catch(function () {
notification.dismiss();
notificationService.error("Error exporting CSV");
});

View File

@@ -35,13 +35,11 @@ define([
* @constructor
* @memberof {platform/features/timeline}
* @param exportService the service used to export as CSV
* @param resources the `resources` extension category
* @param {DomainObject} domainObject the timeline being exported
*/
function ExportTimelineAsCSVTask(exportService, resources, domainObject) {
function ExportTimelineAsCSVTask(exportService, domainObject) {
this.domainObject = domainObject;
this.exportService = exportService;
this.resources = resources;
}
/**
@@ -52,10 +50,9 @@ define([
*/
ExportTimelineAsCSVTask.prototype.run = function () {
var exportService = this.exportService;
var resources = this.resources;
function doExport(objects) {
var exporter = new TimelineColumnizer(objects, resources),
var exporter = new TimelineColumnizer(objects),
options = { headers: exporter.headers() };
return exporter.rows().then(function (rows) {
return exportService.exportCSV(rows, options);

View File

@@ -23,23 +23,19 @@
define([], function () {
/**
* A column showing identifying domain objects.
* A column showing domain object identifiers.
* @constructor
* @param idMap an object containing key value pairs, where keys
* are domain object identifiers and values are whatever
* should appear in CSV output in their place
* @implements {platform/features/timeline.TimelineCSVColumn}
*/
function IdColumn(idMap) {
this.idMap = idMap;
function IdColumn() {
}
IdColumn.prototype.name = function () {
return "Index";
return "Identifier";
};
IdColumn.prototype.value = function (domainObject) {
return this.idMap[domainObject.getId()];
return domainObject.getId();
};
return IdColumn;

View File

@@ -27,14 +27,10 @@ define([], function () {
* @constructor
* @param {number} index the zero-based index of the composition
* element associated with this column
* @param idMap an object containing key value pairs, where keys
* are domain object identifiers and values are whatever
* should appear in CSV output in their place
* @implements {platform/features/timeline.TimelineCSVColumn}
*/
function ModeColumn(index, idMap) {
function ModeColumn(index) {
this.index = index;
this.idMap = idMap;
}
ModeColumn.prototype.name = function () {
@@ -43,9 +39,8 @@ define([], function () {
ModeColumn.prototype.value = function (domainObject) {
var model = domainObject.getModel(),
modes = (model.relationships || {}).modes || [];
return modes.length > this.index ?
this.idMap[modes[this.index]] : "";
composition = (model.relationships || {}).modes || [];
return (composition[this.index]) || "";
};
return ModeColumn;

View File

@@ -25,15 +25,13 @@ define([
"./ModeColumn",
"./CompositionColumn",
"./MetadataColumn",
"./TimespanColumn",
"./UtilizationColumn"
"./TimespanColumn"
], function (
IdColumn,
ModeColumn,
CompositionColumn,
MetadataColumn,
TimespanColumn,
UtilizationColumn
TimespanColumn
) {
/**
@@ -65,17 +63,15 @@ define([
*
* @param {DomainObject[]} domainObjects the objects to include
* in the exported data
* @param {Array} resources an array of `resources` extensions
* @constructor
* @memberof {platform/features/timeline}
*/
function TimelineColumnizer(domainObjects, resources) {
function TimelineColumnizer(domainObjects) {
var maxComposition = 0,
maxRelationships = 0,
columnNames = {},
columns = [],
foundTimespan = false,
idMap,
i;
function addMetadataProperty(property) {
@@ -86,12 +82,7 @@ define([
}
}
idMap = domainObjects.reduce(function (map, domainObject, index) {
map[domainObject.getId()] = index + 1;
return map;
}, {});
columns.push(new IdColumn(idMap));
columns.push(new IdColumn());
domainObjects.forEach(function (domainObject) {
var model = domainObject.getModel(),
@@ -122,16 +113,12 @@ define([
columns.push(new TimespanColumn(false));
}
resources.forEach(function (resource) {
columns.push(new UtilizationColumn(resource));
});
for (i = 0; i < maxComposition; i += 1) {
columns.push(new CompositionColumn(i, idMap));
columns.push(new CompositionColumn(i));
}
for (i = 0; i < maxRelationships; i += 1) {
columns.push(new ModeColumn(i, idMap));
columns.push(new ModeColumn(i));
}
this.domainObjects = domainObjects;

View File

@@ -1,72 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2009-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* A column showing utilization costs associated with activities.
* @constructor
* @param {string} key the key for the particular cost
* @implements {platform/features/timeline.TimelineCSVColumn}
*/
function UtilizationColumn(resource) {
this.resource = resource;
}
UtilizationColumn.prototype.name = function () {
var units = {
"Kbps": "Kb",
"watts": "watt-seconds"
}[this.resource.units] || "unknown units";
return this.resource.name + " (" + units + ")";
};
UtilizationColumn.prototype.value = function (domainObject) {
var resource = this.resource;
function getCost(utilization) {
var seconds = (utilization.end - utilization.start) / 1000;
return seconds * utilization.value;
}
function getUtilizationValue(utilizations) {
utilizations = utilizations.filter(function (utilization) {
return utilization.key === resource.key;
});
if (utilizations.length === 0) {
return "";
}
return utilizations.map(getCost).reduce(function (a, b) {
return a + b;
}, 0);
}
return domainObject.hasCapability('utilization') ?
domainObject.getCapability('utilization').internal()
.then(getUtilizationValue) :
"";
};
return UtilizationColumn;
});

View File

@@ -193,13 +193,6 @@ define(
* @returns {Promise.<string[]>} a promise for resource identifiers
*/
resources: promiseResourceKeys,
/**
* Get the resource utilization associated with this object
* directly, not including any resource utilization associated
* with contained objects.
* @returns {Promise.<Array>}
*/
internal: promiseInternalUtilization,
/**
* Get the resource utilization associated with this
* object. Results are not sorted. This requires looking

View File

@@ -79,6 +79,15 @@ define(
graphPopulator.populate(swimlanePopulator.get());
}
// Get pixel width for right pane, using zoom controller
function width(zoomController) {
var start = swimlanePopulator.start(),
end = swimlanePopulator.end();
return zoomController.toPixels(zoomController.duration(
Math.max(end - start, MINIMUM_DURATION)
));
}
// Refresh resource graphs
function refresh() {
if (graphPopulator) {
@@ -112,10 +121,10 @@ define(
// Expose active set of swimlanes
return {
/**
* Get the end of the displayed timeline, in milliseconds.
* @returns {number} the end of the displayed timeline
* Get the width, in pixels, of the timeline area
* @returns {number} width, in pixels
*/
end: swimlanePopulator.end.bind(swimlanePopulator),
width: width,
/**
* Get the swimlanes which should currently be displayed.
* @returns {TimelineSwimlane[]} the swimlanes

View File

@@ -22,17 +22,27 @@
define(
[],
function () {
var PADDING = 0.25;
/**
* Controls the pan-zoom state of a timeline view.
* @constructor
*/
function TimelineZoomController($scope, $window, ZOOM_CONFIGURATION) {
function TimelineZoomController($scope, ZOOM_CONFIGURATION) {
// Prefer to start with the middle index
var zoomLevels = ZOOM_CONFIGURATION.levels || [1000],
zoomIndex = Math.floor(zoomLevels.length / 2),
tickWidth = ZOOM_CONFIGURATION.width || 200;
tickWidth = ZOOM_CONFIGURATION.width || 200,
bounds = { x: 0, width: tickWidth },
duration = 86400000; // Default duration in view
// Round a duration to a larger value, to ensure space for editing
function roundDuration(value) {
// Ensure there's always an extra day or so
var tickCount = bounds.width / tickWidth,
sz = zoomLevels[zoomLevels.length - 1] * tickCount;
value *= 1.25; // Add 25% padding to start
return Math.ceil(value / sz) * sz;
}
function toMillis(pixels) {
return (pixels / tickWidth) * zoomLevels[zoomIndex];
@@ -53,21 +63,14 @@ define(
}
}
function setScroll(x) {
$window.requestAnimationFrame(function () {
$scope.scroll.x = x;
$scope.$apply();
});
}
function initializeZoomFromTimespan(timespan) {
var timelineDuration = timespan.getDuration();
zoomIndex = 0;
while (toMillis($scope.scroll.width) < timelineDuration &&
while (toMillis(bounds.width) < timelineDuration &&
zoomIndex < zoomLevels.length - 1) {
zoomIndex += 1;
}
setScroll(toPixels(timespan.getStart()));
bounds.x = toPixels(timespan.getStart());
}
function initializeZoom() {
@@ -77,6 +80,9 @@ define(
}
}
$scope.$watch("scroll", function (scroll) {
bounds = scroll;
});
$scope.$watch("domainObject", initializeZoom);
return {
@@ -94,10 +100,9 @@ define(
zoom: function (amount) {
// Update the zoom level if called with an argument
if (arguments.length > 0 && !isNaN(amount)) {
var bounds = $scope.scroll;
var center = this.toMillis(bounds.x + bounds.width / 2);
setZoomLevel(zoomIndex + amount);
setScroll(this.toPixels(center) - bounds.width / 2);
bounds.x = this.toPixels(center) - bounds.width / 2;
}
return zoomLevels[zoomIndex];
},
@@ -119,14 +124,16 @@ define(
*/
toMillis: toMillis,
/**
* Get the pixel width necessary to fit the specified
* timestamp, expressed as an offset in milliseconds from
* the start of the timeline.
* @param {number} timestamp the time to display
* Get or set the current displayed duration. If used as a
* setter, this will typically be rounded up to ensure extra
* space is available at the right.
* @returns {number} duration, in milliseconds
*/
width: function (timestamp) {
var pixels = Math.ceil(toPixels(timestamp * (1 + PADDING)));
return Math.max($scope.scroll.width, pixels);
duration: function (value) {
if (arguments.length > 0) {
duration = roundDuration(value);
}
return duration;
}
};
}

View File

@@ -23,20 +23,13 @@
define(
['../../src/actions/CompositionColumn'],
function (CompositionColumn) {
var TEST_IDS = ['a', 'b', 'c', 'd', 'e', 'f'];
describe("CompositionColumn", function () {
var testIndex,
testIdMap,
column;
beforeEach(function () {
testIndex = 3;
testIdMap = TEST_IDS.reduce(function (map, id, index) {
map[id] = index;
return map;
}, {});
column = new CompositionColumn(testIndex, testIdMap);
column = new CompositionColumn(testIndex);
});
it("includes a one-based index in its name", function () {
@@ -53,13 +46,15 @@ define(
'domainObject',
['getId', 'getModel', 'getCapability']
);
testModel = { composition: TEST_IDS };
testModel = {
composition: ['a', 'b', 'c', 'd', 'e', 'f']
};
mockDomainObject.getModel.andReturn(testModel);
});
it("returns a corresponding value from the map", function () {
it("returns a corresponding identifier", function () {
expect(column.value(mockDomainObject))
.toEqual(testIdMap[testModel.composition[testIndex]]);
.toEqual(testModel.composition[testIndex]);
});
it("returns nothing when composition is exceeded", function () {

View File

@@ -24,8 +24,7 @@ define(
['../../src/actions/ExportTimelineAsCSVAction'],
function (ExportTimelineAsCSVAction) {
describe("ExportTimelineAsCSVAction", function () {
var mockLog,
mockExportService,
var mockExportService,
mockNotificationService,
mockNotification,
mockDomainObject,
@@ -40,13 +39,6 @@ define(
['getId', 'getModel', 'getCapability', 'hasCapability']
);
mockType = jasmine.createSpyObj('type', ['instanceOf']);
mockLog = jasmine.createSpyObj('$log', [
'warn',
'error',
'info',
'debug'
]);
mockExportService = jasmine.createSpyObj(
'exportService',
['exportCSV']
@@ -71,10 +63,8 @@ define(
testContext = { domainObject: mockDomainObject };
action = new ExportTimelineAsCSVAction(
mockLog,
mockExportService,
mockNotificationService,
[],
testContext
);
});
@@ -139,11 +129,8 @@ define(
});
describe("and an error occurs", function () {
var testError;
beforeEach(function () {
testError = { someProperty: "some value" };
testPromise.reject(testError);
testPromise.reject();
waitsFor(function () {
return mockCallback.calls.length > 0;
});
@@ -158,10 +145,6 @@ define(
expect(mockNotificationService.error)
.toHaveBeenCalledWith(jasmine.any(String));
});
it("logs the root cause", function () {
expect(mockLog.warn).toHaveBeenCalledWith(testError);
});
});
});
});

View File

@@ -52,7 +52,6 @@ define(
task = new ExportTimelineAsCSVTask(
mockExportService,
[],
mockDomainObject
);
});

View File

@@ -24,12 +24,10 @@ define(
['../../src/actions/IdColumn'],
function (IdColumn) {
describe("IdColumn", function () {
var testIdMap,
column;
var column;
beforeEach(function () {
testIdMap = { "foo": "bar" };
column = new IdColumn(testIdMap);
column = new IdColumn();
});
it("has a name", function () {
@@ -49,9 +47,9 @@ define(
mockDomainObject.getId.andReturn(testId);
});
it("provides a value mapped from domain object's identifier", function () {
it("provides a domain object's identifier", function () {
expect(column.value(mockDomainObject))
.toEqual(testIdMap[testId]);
.toEqual(testId);
});
});

View File

@@ -23,20 +23,13 @@
define(
['../../src/actions/ModeColumn'],
function (ModeColumn) {
var TEST_IDS = ['a', 'b', 'c', 'd', 'e', 'f'];
describe("ModeColumn", function () {
var testIndex,
testIdMap,
column;
beforeEach(function () {
testIndex = 3;
testIdMap = TEST_IDS.reduce(function (map, id, index) {
map[id] = index;
return map;
}, {});
column = new ModeColumn(testIndex, testIdMap);
column = new ModeColumn(testIndex);
});
it("includes a one-based index in its name", function () {
@@ -55,15 +48,15 @@ define(
);
testModel = {
relationships: {
modes: TEST_IDS
modes: ['a', 'b', 'c', 'd', 'e', 'f']
}
};
mockDomainObject.getModel.andReturn(testModel);
});
it("returns a corresponding value from the map", function () {
it("returns a corresponding identifier", function () {
expect(column.value(mockDomainObject))
.toEqual(testIdMap[testModel.relationships.modes[testIndex]]);
.toEqual(testModel.relationships.modes[testIndex]);
});
it("returns nothing when relationships are exceeded", function () {

View File

@@ -75,7 +75,7 @@ define(
return c === 'metadata' && testMetadata;
});
exporter = new TimelineColumnizer(mockDomainObjects, []);
exporter = new TimelineColumnizer(mockDomainObjects);
});
describe("rows", function () {
@@ -94,6 +94,13 @@ define(
it("include one row per domain object", function () {
expect(rows.length).toEqual(mockDomainObjects.length);
});
it("includes identifiers for each domain object", function () {
rows.forEach(function (row, index) {
var id = mockDomainObjects[index].getId();
expect(row.indexOf(id)).not.toEqual(-1);
});
});
});
describe("headers", function () {

View File

@@ -214,6 +214,23 @@ define(
});
it("reports full scrollable width using zoom controller", function () {
var mockZoom = jasmine.createSpyObj('zoom', ['toPixels', 'duration']);
mockZoom.toPixels.andReturn(54321);
mockZoom.duration.andReturn(12345);
// Initially populate
fireWatch('domainObject', mockDomainObject);
expect(controller.width(mockZoom)).toEqual(54321);
// Verify interactions; we took zoom's duration for our start/end,
// and converted it to pixels.
// First, check that we used the start/end (from above)
expect(mockZoom.duration).toHaveBeenCalledWith(12321 - 42);
// Next, verify that the result was passed to toPixels
expect(mockZoom.toPixels).toHaveBeenCalledWith(12345);
});
it("provides drag handles", function () {
// TimelineDragPopulator et al are tested for these,
// so just verify that handles are indeed exposed.

View File

@@ -28,7 +28,6 @@ define(
describe("The timeline zoom state controller", function () {
var testConfiguration,
mockScope,
mockWindow,
controller;
beforeEach(function () {
@@ -36,16 +35,10 @@ define(
levels: [1000, 2000, 3500],
width: 12321
};
mockScope =
jasmine.createSpyObj("$scope", ['$watch', '$apply']);
mockScope = jasmine.createSpyObj("$scope", ['$watch']);
mockScope.commit = jasmine.createSpy('commit');
mockScope.scroll = { x: 0, width: 1000 };
mockWindow = {
requestAnimationFrame: jasmine.createSpy('raf')
};
controller = new TimelineZoomController(
mockScope,
mockWindow,
testConfiguration
);
});
@@ -54,6 +47,12 @@ define(
expect(controller.zoom()).toEqual(2000);
});
it("allows duration to be changed", function () {
var initial = controller.duration();
controller.duration(initial * 3.33);
expect(controller.duration() > initial).toBeTruthy();
});
it("handles time-to-pixel conversions", function () {
var zoomLevel = controller.zoom();
expect(controller.toPixels(zoomLevel)).toEqual(12321);
@@ -71,6 +70,11 @@ define(
expect(controller.zoom()).toEqual(3500);
});
it("observes scroll bounds", function () {
expect(mockScope.$watch)
.toHaveBeenCalledWith("scroll", jasmine.any(Function));
});
describe("when watches have fired", function () {
var mockDomainObject,
mockPromise,
@@ -111,10 +115,6 @@ define(
mockScope.$watch.calls.forEach(function (call) {
call.args[1](mockScope[call.args[0]]);
});
mockWindow.requestAnimationFrame.calls.forEach(function (call) {
call.args[0]();
});
});
it("zooms to fit the timeline", function () {
@@ -125,27 +125,6 @@ define(
expect(Math.round(controller.toMillis(x2)))
.toBeGreaterThan(testEnd);
});
it("provides a width which is not less than scroll area width", function () {
var testPixel = mockScope.scroll.width / 4,
testMillis = controller.toMillis(testPixel);
expect(controller.width(testMillis))
.not.toBeLessThan(mockScope.scroll.width);
});
it("provides a width with some margin past timestamp", function () {
var testPixel = mockScope.scroll.width * 4,
testMillis = controller.toMillis(testPixel);
expect(controller.width(testMillis))
.toBeGreaterThan(controller.toPixels(testMillis));
});
it("provides a width which does not greatly exceed timestamp", function () {
var testPixel = mockScope.scroll.width * 4,
testMillis = controller.toMillis(testPixel);
expect(controller.width(testMillis))
.toBeLessThan(controller.toPixels(testMillis * 2));
});
});
});

View File

@@ -61,7 +61,8 @@ define(
{
x: event.pageX - rect.left,
y: event.pageY - rect.top
}
},
domainObject
);
}
}

View File

@@ -34,7 +34,8 @@ define(
TEST_ID = "test-id",
DROP_ID = "drop-id";
describe("The drop gesture", function () {
//TODO: Disabled for NEM Beta
xdescribe("The drop gesture", function () {
var mockDndService,
mockQ,
mockElement,
@@ -143,6 +144,23 @@ define(
expect(mockCompose.perform).toHaveBeenCalled();
});
it("does not invoke compose on drop in browse mode for non-folders", function () {
// Set the mockDomainObject to not have the editor capability
mockDomainObject.hasCapability.andReturn(false);
// Set the mockDomainObject to not have a type of folder
mockDomainObject.getModel.andReturn({type: 'notAFolder'});
callbacks.dragover(mockEvent);
expect(mockAction.getActions).toHaveBeenCalledWith({
key: 'compose',
selectedObject: mockDraggedObject
});
callbacks.drop(mockEvent);
expect(mockCompose.perform).not.toHaveBeenCalled();
});
it("invokes compose on drop in browse mode for folders", function () {
// Set the mockDomainObject to not have the editor capability
mockDomainObject.hasCapability.andReturn(false);

View File

@@ -34,8 +34,8 @@ var EditItem = (function () {
EditItem.prototype.EditButton = function () {
return element.all(by.css('[ng-click="parameters.action.perform()"]')).filter(function (arg) {
return arg.getAttribute("title").then(function (title){
//expect(title).toEqual("Edit");
return title == 'Edit';
//expect(title).toEqual("Edit this object.");
return title == 'Edit this object.';
})
});
};

36
src/MCT.js Normal file
View File

@@ -0,0 +1,36 @@
define([
'EventEmitter',
'legacyRegistry',
'./api/api',
'./api/objects/bundle'
], function (
EventEmitter,
legacyRegistry,
api
) {
function MCT() {
EventEmitter.call(this);
this.legacyBundle = { extensions: {} };
}
MCT.prototype = Object.create(EventEmitter.prototype);
Object.keys(api).forEach(function (k) {
MCT.prototype[k] = api[k];
});
MCT.prototype.type = function (key, type) {
var legacyDef = type.toLegacyDefinition();
legacyDef.key = key;
this.legacyBundle.extensions.types =
this.legacyBundle.extensions.types || [];
this.legacyBundle.extensions.types.push(legacyDef);
};
MCT.prototype.start = function () {
legacyRegistry.register('adapter', this.legacyBundle);
this.emit('start');
};
return MCT;
});

43
src/api/Type.js Normal file
View File

@@ -0,0 +1,43 @@
define(function () {
/**
* @typedef TypeDefinition
* @property {Metadata} metadata displayable metadata about this type
* @property {function (object)} [initialize] a function which initializes
* the model for new domain objects of this type
* @property {boolean} [creatable] true if users should be allowed to
* create this type (default: false)
*/
/**
*
* @param {TypeDefinition} definition
* @constructor
*/
function Type(definition) {
this.definition = definition;
}
/**
* Get a definition for this type that can be registered using the
* legacy bundle format.
* @private
*/
Type.prototype.toLegacyDefinition = function () {
var def = {};
def.name = this.definition.metadata.label;
def.glyph = this.definition.metadata.glyph;
def.description = this.definition.metadata.description;
if (this.definition.initialize) {
def.model = {};
this.definition.initialize(def.model);
}
if (this.definition.creatable) {
def.features = ['creation'];
}
return def;
};
return Type;
});

12
src/api/api.js Normal file
View File

@@ -0,0 +1,12 @@
define([
'./Type',
'./objects/ObjectAPI'
], function (
Type,
ObjectAPI
) {
return {
Type: Type,
Objects: ObjectAPI
};
});

View File

@@ -0,0 +1,70 @@
define([
'./object-utils',
'./ObjectAPI'
], function (
utils,
ObjectAPI
) {
function ObjectServiceProvider(objectService, instantiate) {
this.objectService = objectService;
this.instantiate = instantiate;
}
ObjectServiceProvider.prototype.save = function (object) {
var key = object.key,
keyString = utils.makeKeyString(key),
newObject = this.instantiate(utils.toOldFormat(object), keyString);
return object.getCapability('persistence')
.persist()
.then(function () {
return utils.toNewFormat(object, key);
});
};
ObjectServiceProvider.prototype.delete = function (object) {
// TODO!
};
ObjectServiceProvider.prototype.get = function (key) {
var keyString = utils.makeKeyString(key);
return this.objectService.getObjects([keyString])
.then(function (results) {
var model = JSON.parse(JSON.stringify(results[keyString].getModel()));
return utils.toNewFormat(model, key);
});
};
// Injects new object API as a decorator so that it hijacks all requests.
// Object providers implemented on new API should just work, old API should just work, many things may break.
function LegacyObjectAPIInterceptor(ROOTS, instantiate, objectService) {
this.getObjects = function (keys) {
var results = {},
promises = keys.map(function (keyString) {
var key = utils.parseKeyString(keyString);
return ObjectAPI.get(key)
.then(function (object) {
object = utils.toOldFormat(object)
results[keyString] = instantiate(object, keyString);
});
});
return Promise.all(promises)
.then(function () {
return results;
});
};
ObjectAPI._supersecretSetFallbackProvider(
new ObjectServiceProvider(objectService, instantiate)
);
ROOTS.forEach(function (r) {
ObjectAPI.addRoot(utils.parseKeyString(r.id));
});
return this;
}
return LegacyObjectAPIInterceptor;
});

View File

@@ -0,0 +1,92 @@
define([
'lodash',
'./object-utils'
], function (
_,
utils
) {
/**
Object API. Intercepts the existing object API while also exposing
A new Object API.
MCT.objects.get('mine')
.then(function (root) {
console.log(root);
MCT.objects.getComposition(root)
.then(function (composition) {
console.log(composition)
})
});
*/
var Objects = {},
ROOT_REGISTRY = [],
PROVIDER_REGISTRY = {},
FALLBACK_PROVIDER;
Objects._supersecretSetFallbackProvider = function (p) {
FALLBACK_PROVIDER = p;
};
// Root provider is hardcoded in; can't be skipped.
var RootProvider = {
'get': function () {
return Promise.resolve({
name: 'The root object',
type: 'root',
composition: ROOT_REGISTRY
});
}
};
// Retrieve the provider for a given key.
function getProvider(key) {
if (key.identifier === 'ROOT') {
return RootProvider;
}
return PROVIDER_REGISTRY[key.namespace] || FALLBACK_PROVIDER;
};
Objects.addProvider = function (namespace, provider) {
PROVIDER_REGISTRY[namespace] = provider;
};
[
'save',
'delete',
'get'
].forEach(function (method) {
Objects[method] = function () {
var key = arguments[0],
provider = getProvider(key);
if (!provider) {
throw new Error('No Provider Matched');
}
if (!provider[method]) {
throw new Error('Provider does not support [' + method + '].');
}
return provider[method].apply(provider, arguments);
};
});
Objects.addRoot = function (key) {
ROOT_REGISTRY.unshift(key);
};
Objects.removeRoot = function (key) {
ROOT_REGISTRY = ROOT_REGISTRY.filter(function (k) {
return (
k.identifier !== key.identifier ||
k.namespace !== key.namespace
);
});
};
return Objects;
});

100
src/api/objects/README.md Normal file
View File

@@ -0,0 +1,100 @@
# Object API - Overview
The object API provides methods for fetching domain objects.
# Keys
Keys are a composite identifier that is used to create and persist objects. Ex:
```javascript
{
namespace: 'elastic',
identifier: 'myIdentifier'
}
```
In old MCT days, we called this an "id", and we encoded it in a single string.
The above key would encode into the identifier, `elastic:myIdentifier`.
When interacting with the API you will be dealing with key objects.
# Configuring the Object API
The following methods should be used before calling run. They allow you to
configure the persistence space of MCT.
* `MCT.objects.addRoot(key)` -- add a "ROOT" to Open MCT by specifying it's
key.
* `MCT.objects.removeRoot(key)` -- Remove a "ROOT" from Open MCT by key.
* `MCT.objects.addProvider(namespace, provider)` -- register an object provider
for a specific namespace. See below for documentation on the provider
interface.
# Using the object API
The object API provides methods for getting, saving, and deleting objects.
* MCT.objects.get(key) -> returns promise for an object
* MCT.objects.save(object) -> returns promise that is resolved when object
has been saved
* MCT.objects.delete(object) -> returns promise that is resolved when object has
been deleted
## Configuration Example: Adding a groot
The following example adds a new root object for groot and populates it with
some pieces of groot.
```javascript
var ROOT_KEY = {
namespace: 'groot',
identifier: 'groot'
};
var GROOT_ROOT = {
name: 'I am groot',
type: 'folder',
composition: [
{
namespace: 'groot',
identifier: 'arms'
},
{
namespace: 'groot',
identifier: 'legs'
},
{
namespace: 'groot',
identifier: 'torso'
}
]
};
var GrootProvider = {
get: function (key) {
if (key.identifier === 'groot') {
return Promise.resolve(GROOT_ROOT);
}
return Promise.resolve({
name: 'Groot\'s ' + key.identifier
});
}
};
mct.Objects.addRoot(ROOT_KEY);
mct.Objects.addProvider('groot', GrootProvider);
```
### Making a custom provider:
All methods on the provider interface are optional, so you do not need
to modify them.
* `provider.get(key)` -> promise for a domain object.
* `provider.save(domainObject)` -> returns promise that is fulfilled when object
has been saved.
* `provider.delete(domainObject)` -> returns promise that is fulfilled when
object has been deleted.

49
src/api/objects/bundle.js Normal file
View File

@@ -0,0 +1,49 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
'./LegacyObjectAPIInterceptor',
'legacyRegistry'
], function (
LegacyObjectAPIInterceptor,
legacyRegistry
) {
legacyRegistry.register('src/api/objects', {
name: 'Object API',
description: 'The public Objects API',
extensions: {
components: [
{
provides: "objectService",
type: "decorator",
priority: "mandatory",
implementation: LegacyObjectAPIInterceptor,
depends: [
"roots[]",
"instantiate"
]
}
]
}
});
});

View File

@@ -0,0 +1,67 @@
define([
], function (
) {
// take a key string and turn it into a key object
// 'scratch:root' ==> {namespace: 'scratch', identifier: 'root'}
var parseKeyString = function (key) {
if (typeof key === 'object') {
return key;
}
var namespace = '',
identifier = key;
for (var i = 0, escaped = false, len=key.length; i < len; i++) {
if (key[i] === ":" && !escaped) {
namespace = key.slice(0, i);
identifier = key.slice(i + 1);
break;
}
}
return {
namespace: namespace,
identifier: identifier
};
};
// take a key and turn it into a key string
// {namespace: 'scratch', identifier: 'root'} ==> 'scratch:root'
var makeKeyString = function (key) {
if (typeof key === 'string') {
return key;
}
if (!key.namespace) {
return key.identifier;
}
return [
key.namespace.replace(':', '\\:'),
key.identifier.replace(':', '\\:')
].join(':');
};
// Converts composition to use key strings instead of keys
var toOldFormat = function (model) {
delete model.key;
if (model.composition) {
model.composition = model.composition.map(makeKeyString);
}
return model;
};
// converts composition to use keys instead of key strings
var toNewFormat = function (model, key) {
model.key = key;
if (model.composition) {
model.composition = model.composition.map(parseKeyString);
}
return model;
};
return {
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString
};
});

View File

@@ -48,6 +48,7 @@ requirejs.config({
"angular-route": "bower_components/angular-route/angular-route.min",
"csv": "bower_components/comma-separated-values/csv.min",
"es6-promise": "bower_components/es6-promise/promise.min",
"EventEmitter": "bower_components/eventemitter3/index",
"moment": "bower_components/moment/moment",
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
@@ -64,6 +65,9 @@ requirejs.config({
"angular-route": {
"deps": [ "angular" ]
},
"EventEmitter": {
"exports": "EventEmitter"
},
"moment-duration-format": {
"deps": [ "moment" ]
},

127
tutorial-server/app.js Normal file
View File

@@ -0,0 +1,127 @@
/*global require,process,console*/
var CONFIG = {
port: 8081,
dictionary: "dictionary.json",
interval: 1000
};
(function () {
"use strict";
var WebSocketServer = require('ws').Server,
fs = require('fs'),
wss = new WebSocketServer({ port: CONFIG.port }),
dictionary = JSON.parse(fs.readFileSync(CONFIG.dictionary, "utf8")),
spacecraft = {
"prop.fuel": 77,
"prop.thrusters": "OFF",
"comms.recd": 0,
"comms.sent": 0,
"pwr.temp": 245,
"pwr.c": 8.15,
"pwr.v": 30
},
histories = {},
listeners = [];
function updateSpacecraft() {
spacecraft["prop.fuel"] = Math.max(
0,
spacecraft["prop.fuel"] -
(spacecraft["prop.thrusters"] === "ON" ? 0.5 : 0)
);
spacecraft["pwr.temp"] = spacecraft["pwr.temp"] * 0.985
+ Math.random() * 0.25 + Math.sin(Date.now());
spacecraft["pwr.c"] = spacecraft["pwr.c"] * 0.985;
spacecraft["pwr.v"] = 30 + Math.pow(Math.random(), 3);
}
function generateTelemetry() {
var timestamp = Date.now(), sent = 0;
Object.keys(spacecraft).forEach(function (id) {
var state = { timestamp: timestamp, value: spacecraft[id] };
histories[id] = histories[id] || []; // Initialize
histories[id].push(state);
spacecraft["comms.sent"] += JSON.stringify(state).length;
});
listeners.forEach(function (listener) {
listener();
});
}
function update() {
updateSpacecraft();
generateTelemetry();
}
function handleConnection(ws) {
var subscriptions = {}, // Active subscriptions for this connection
handlers = { // Handlers for specific requests
dictionary: function () {
ws.send(JSON.stringify({
type: "dictionary",
value: dictionary
}));
},
subscribe: function (id) {
subscriptions[id] = true;
},
unsubscribe: function (id) {
delete subscriptions[id];
},
history: function (id) {
ws.send(JSON.stringify({
type: "history",
id: id,
value: histories[id]
}));
}
};
function notifySubscribers() {
Object.keys(subscriptions).forEach(function (id) {
var history = histories[id];
if (history) {
ws.send(JSON.stringify({
type: "data",
id: id,
value: history[history.length - 1]
}));
}
});
}
// Listen for requests
ws.on('message', function (message) {
var parts = message.split(' '),
handler = handlers[parts[0]];
if (handler) {
handler.apply(handlers, parts.slice(1));
}
});
// Stop sending telemetry updates for this connection when closed
ws.on('close', function () {
listeners = listeners.filter(function (listener) {
return listener !== notifySubscribers;
});
});
// Notify subscribers when telemetry is updated
listeners.push(notifySubscribers);
}
update();
setInterval(update, CONFIG.interval);
wss.on('connection', handleConnection);
console.log("Example spacecraft running on port ");
console.log("Press Enter to toggle thruster state.");
process.stdin.on('data', function (data) {
spacecraft['prop.thrusters'] =
(spacecraft['prop.thrusters'] === "OFF") ? "ON" : "OFF";
console.log("Thrusters " + spacecraft["prop.thrusters"]);
});
}());

View File

@@ -0,0 +1,66 @@
{
"name": "Example Spacecraft",
"identifier": "sc",
"subsystems": [
{
"name": "Propulsion",
"identifier": "prop",
"measurements": [
{
"name": "Fuel",
"identifier": "prop.fuel",
"units": "kilograms",
"type": "float"
},
{
"name": "Thrusters",
"identifier": "prop.thrusters",
"units": "None",
"type": "string"
}
]
},
{
"name": "Communications",
"identifier": "comms",
"measurements": [
{
"name": "Received",
"identifier": "comms.recd",
"units": "bytes",
"type": "integer"
},
{
"name": "Sent",
"identifier": "comms.sent",
"units": "bytes",
"type": "integer"
}
]
},
{
"name": "Power",
"identifier": "pwr",
"measurements": [
{
"name": "Generator Temperature",
"identifier": "pwr.temp",
"units": "\u0080C",
"type": "float"
},
{
"name": "Generator Current",
"identifier": "pwr.c",
"units": "A",
"type": "float"
},
{
"name": "Generator Voltage",
"identifier": "pwr.v",
"units": "V",
"type": "float"
}
]
}
]
}

View File

@@ -0,0 +1,66 @@
define([
'legacyRegistry',
'./src/controllers/BarGraphController'
], function (
legacyRegistry,
BarGraphController
) {
legacyRegistry.register("tutorials/bargraph", {
"name": "Bar Graph",
"description": "Provides the Bar Graph view of telemetry elements.",
"extensions": {
"views": [
{
"name": "Bar Graph",
"key": "example.bargraph",
"glyph": "H",
"templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ],
"delegation": true,
"editable": true,
"toolbar": {
"sections": [
{
"items": [
{
"name": "Low",
"property": "low",
"required": true,
"control": "textfield",
"size": 4
},
{
"name": "Middle",
"property": "middle",
"required": true,
"control": "textfield",
"size": 4
},
{
"name": "High",
"property": "high",
"required": true,
"control": "textfield",
"size": 4
}
]
}
]
}
}
],
"stylesheets": [
{
"stylesheetUrl": "css/bargraph.css"
}
],
"controllers": [
{
"key": "BarGraphController",
"implementation": BarGraphController,
"depends": [ "$scope", "telemetryHandler" ]
}
]
}
});
});

View File

@@ -0,0 +1,35 @@
<div class="example-bargraph" ng-controller="BarGraphController">
<div class="example-tick-labels">
<div ng-repeat="value in [low, middle, high] track by $index"
class="example-tick-label"
style="bottom: {{ toPercent(value) }}%">
{{value}}
</div>
</div>
<div class="example-graph-area">
<div ng-repeat="telemetryObject in telemetryObjects"
style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
class="example-bar-holder">
<div class="example-bar"
ng-style="{
bottom: getBottom(telemetryObject) + '%',
top: getTop(telemetryObject) + '%'
}">
</div>
</div>
<div style="bottom: {{ toPercent(middle) }}%"
class="example-graph-tick">
</div>
</div>
<div class="example-bar-labels">
<div ng-repeat="telemetryObject in telemetryObjects"
style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
class="example-bar-holder example-label">
<mct-representation key="'label'"
mct-object="telemetryObject">
</mct-representation>
</div>
</div>
</div>

View File

@@ -0,0 +1,75 @@
define(function () {
function BarGraphController($scope, telemetryHandler) {
var handle;
// Expose configuration constants directly in scope
function exposeConfiguration() {
$scope.low = $scope.configuration.low;
$scope.middle = $scope.configuration.middle;
$scope.high = $scope.configuration.high;
}
// Populate a default value in the configuration
function setDefault(key, value) {
if ($scope.configuration[key] === undefined) {
$scope.configuration[key] = value;
}
}
// Getter-setter for configuration properties (for view proxy)
function getterSetter(property) {
return function (value) {
value = parseFloat(value);
if (!isNaN(value)) {
$scope.configuration[property] = value;
exposeConfiguration();
}
return $scope.configuration[property];
};
}
// Add min/max defaults
setDefault('low', -1);
setDefault('middle', 0);
setDefault('high', 1);
exposeConfiguration($scope.configuration);
// Expose view configuration options
if ($scope.selection) {
$scope.selection.proxy({
low: getterSetter('low'),
middle: getterSetter('middle'),
high: getterSetter('high')
});
}
// Convert value to a percent between 0-100
$scope.toPercent = function (value) {
var pct = 100 * (value - $scope.low) /
($scope.high - $scope.low);
return Math.min(100, Math.max(0, pct));
};
// Get bottom and top (as percentages) for current value
$scope.getBottom = function (telemetryObject) {
var value = handle.getRangeValue(telemetryObject);
return $scope.toPercent(Math.min($scope.middle, value));
};
$scope.getTop = function (telemetryObject) {
var value = handle.getRangeValue(telemetryObject);
return 100 - $scope.toPercent(Math.max($scope.middle, value));
};
// Use the telemetryHandler to get telemetry objects here
handle = telemetryHandler.handle($scope.domainObject, function () {
$scope.telemetryObjects = handle.getTelemetryObjects();
$scope.barWidth =
100 / Math.max(($scope.telemetryObjects).length, 1);
});
// Release subscriptions when scope is destroyed
$scope.$on('$destroy', handle.unsubscribe);
}
return BarGraphController;
});

View File

@@ -0,0 +1,44 @@
define(function () {
return function grootPlugin(mct) {
var ROOT_KEY = {
namespace: 'groot',
identifier: 'groot'
};
var GROOT_ROOT = {
name: 'I am groot',
type: 'folder',
composition: [
{
namespace: 'groot',
identifier: 'arms'
},
{
namespace: 'groot',
identifier: 'legs'
},
{
namespace: 'groot',
identifier: 'torso'
}
]
};
var GrootProvider = {
get: function (key) {
if (key.identifier === 'groot') {
return Promise.resolve(GROOT_ROOT);
}
return Promise.resolve({
name: 'Groot\'s ' + key.identifier
});
}
};
mct.Objects.addRoot(ROOT_KEY);
mct.Objects.addProvider('groot', GrootProvider);
return mct;
};
});

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