Compare commits

...

51 Commits

Author SHA1 Message Date
f9c93ca022 [Build] Remove SNAPSHOT status
To close sprint Huxley,
https://github.com/nasa/openmct/milestones/Huxley
2016-06-06 09:49:52 -07:00
ee0fa0451a Merge branch 'master' into timeline-zoom-center-936
Conflicts:
	platform/features/timeline/src/controllers/TimelineZoomController.js
	platform/features/timeline/test/controllers/TimelineZoomControllerSpec.js
2016-06-03 09:18:01 -07:00
e86e955682 Merge pull request #993 from nasa/revert-990-timeline-regression-817
Revert "[Timeline] Provide greater initial width"
2016-06-02 17:05:11 -07:00
9913fb48f5 Revert "[Timeline] Provide greater initial width" 2016-06-02 16:54:59 -07:00
71c362f016 Merge pull request #992 from nasa/revert-991-timeline-regression-817
Revert "[Timeline] Update scroll position on timeout"
2016-06-02 16:54:48 -07:00
fa6e8fd5f9 Revert "[Timeline] Update scroll position on timeout" 2016-06-02 16:49:49 -07:00
23b64951f3 [Timeline] Update zoom controller spec
...to reflect changes/simplifications for #936.
2016-06-02 16:42:09 -07:00
99590d18f7 [Timeline] Simplify bounds-tracking 2016-06-02 16:40:07 -07:00
86b31bc040 [Timeline] Simplify scroll-setting 2016-06-02 16:38:11 -07:00
d52bfed1df [Timeline] Always set scroll on timeout
...to allow time for width to increase.
2016-06-02 16:36:09 -07:00
44d6456de1 [Timeline] Set scroll on timeout
Whenever timeline zoom controller sets scroll, check to see if
there may be a width violation that causes scroll to be reset,
and retry on a timeout if so. Fixes #936.
2016-06-02 16:05:28 -07:00
beeefe517a Merge pull request #991 from nasa/timeline-regression-817
[Timeline] Update scroll position on timeout
2016-06-02 15:37:42 -07:00
d02f4041b2 [Timeline] Update scroll position on timeout
Fixes #817
2016-06-02 15:34:32 -07:00
9fd75ff91e Merge pull request #990 from nasa/timeline-regression-817
[Timeline] Provide greater initial width
2016-06-02 14:59:14 -07:00
026ece3956 [Timeline] Provide greater initial width
This avoids starting with a scrollable width too small for the
initial scroll position that the zoom controller selects.
Fixes #817
2016-06-02 14:48:58 -07:00
1d6880c283 Merge pull request #982 from nasa/timeline-scroll-981
[Timeline] Use reasonable width for scroll area
2016-06-02 16:59:08 +01:00
8ddad9bf4c Merge pull request #984 from nasa/open979
[Edit] Fixed issue with cancel action throwing an error. Fixes #979
2016-06-02 10:31:20 +01:00
f167022eea Changed logic of persisted check slightly 2016-06-02 10:26:38 +01:00
b1266abf01 [Edit] Fixed issue with cancel action throwing an error. Fixes #979 2016-06-01 10:41:01 +01:00
5a2d1a746d [Timeline] Add missing semicolon 2016-05-31 16:27:59 -07:00
4f0e3fdf85 [Timeline] Test zoom controller's width 2016-05-31 16:21:40 -07:00
be9f56107c [Timeline] Remove obsolete test cases 2016-05-31 16:15:57 -07:00
787f3815df [Timeline] Expose width from ZoomController
...and ensure that the width exposed is not excessively
large; fixes #981
2016-05-31 16:12:49 -07:00
35d7d9b380 [Timeline] Remove width method
...from TimelineController. Will replace with a more straightforward
call to the zoom controller that uses the exposed end time instead,
to address #981.
2016-05-31 16:05:06 -07:00
dc577d4c24 Merge pull request #974 from nasa/open970
R&I open970: hide nav-to-parent arrow when in Edit mode
2016-05-27 14:56:48 -07:00
8f9308de01 Merge pull request #962 from nasa/csv-export-update-751
[Timeline] Updates to CSV Export
2016-05-27 14:55:59 -07:00
8f7a5e113b Merge pull request #951 from nasa/orphan-navigation-765
[Navigation] Prevent navigation to orphan objects
2016-05-27 14:53:33 -07:00
9820f9d9c5 [Frontend] Mod CSS to properly hide nav-to-parent when editing
fixes #970
Not sure what problem was, but betting this was due to removal
of an ng-class previously in markup;
2016-05-26 12:45:25 -07:00
56ff98cce7 Merge branch 'master' into orphan-navigation-765 2016-05-26 12:35:48 -07:00
dade6b2254 [Timeline] Use positive logic for clarity
https://github.com/nasa/openmct/pull/962#discussion_r64678013
2016-05-26 12:12:52 -07:00
e9cac6eff3 [Timeline] Add JSDoc for new parameter
https://github.com/nasa/openmct/pull/962#discussion_r64677520
2016-05-26 12:12:52 -07:00
5689279954 [Timeline] Add JSDoc for idMap
https://github.com/nasa/openmct/pull/962#discussion_r64676750
https://github.com/nasa/openmct/pull/962#discussion_r64677198
2016-05-26 11:53:16 -07:00
f9fd97230f [Timeline] Satisfy JSHint 2016-05-25 12:37:03 -07:00
536e2290b8 Merge branch 'master' into csv-export-update-751 2016-05-25 12:33:43 -07:00
73b922facf [Timeline] Update specs for indexes instead of ids 2016-05-25 12:32:44 -07:00
ba0d9a186b [Timeline] Account for new argument in spec 2016-05-25 12:26:47 -07:00
80f5cb756d [Timeline] Account for new argument in spec 2016-05-25 12:25:58 -07:00
d7f566088f [Timeline] Update spec to include logging 2016-05-25 12:25:02 -07:00
a3bcaea7f9 [Timeline] Show units in utilization headers 2016-05-25 12:21:58 -07:00
23c71b7218 [Timeline] Include units for utilizations 2016-05-25 12:12:11 -07:00
463f7ccf65 [Timeline] Use indexes instead of UUIDs 2016-05-25 12:07:35 -07:00
87fe407739 Merge branch 'master' into csv-export-update-751 2016-05-25 12:00:43 -07:00
bb4f1ce7cd [Timeline] Include utilization columns 2016-05-25 10:52:25 -07:00
0cc2ba7595 [Timeline] Import UtilizationColumn 2016-05-25 10:38:00 -07:00
8162429106 [Timeline] Pass in resources extensions 2016-05-25 10:37:01 -07:00
ed519d89d7 [Timeline] Log errors during CSV export
#751
2016-05-25 10:35:29 -07:00
0e4f6185b8 Merge branch 'master' into csv-export-update-751
Conflicts:
	platform/features/timeline/bundle.js
2016-05-25 10:29:56 -07:00
1ced47fc2c [Navigation] Prevent navigation to orphan objects
This is particularly useful when a persistence failure has caused
a created object not to be added to its parent container. #765
2016-05-23 14:07:09 -07:00
f16a107105 [Timeline] Inject resources into CSV action 2016-04-15 08:51:22 -07:00
f683ca44a2 [Timeline] Read resource utilizations during CSV export 2016-04-15 08:45:42 -07:00
546cde56a8 [Timeline] Expose internal resource utilization
...to allow this to be exported for CSV, #751
2016-04-15 08:32:34 -07:00
27 changed files with 553 additions and 121 deletions

View File

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

View File

@ -27,6 +27,7 @@ define([
"./src/MenuArrowController",
"./src/navigation/NavigationService",
"./src/navigation/NavigateAction",
"./src/navigation/OrphanNavigationHandler",
"./src/windowing/NewTabAction",
"./src/windowing/FullscreenAction",
"./src/windowing/WindowTitler",
@ -47,6 +48,7 @@ define([
MenuArrowController,
NavigationService,
NavigateAction,
OrphanNavigationHandler,
NewTabAction,
FullscreenAction,
WindowTitler,
@ -253,6 +255,14 @@ define([
"$rootScope",
"$document"
]
},
{
"implementation": OrphanNavigationHandler,
"depends": [
"throttle",
"topic",
"navigationService"
]
}
],
"licenses": [

View File

@ -0,0 +1,75 @@
/*****************************************************************************
* 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

@ -0,0 +1,180 @@
/*****************************************************************************
* 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

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

View File

@ -57,6 +57,15 @@ 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);
});
@ -78,6 +87,20 @@ 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

@ -288,8 +288,9 @@ body.desktop .pane .mini-tab-icon.toggle-pane {
.left {
padding-right: $interiorMarginLg;
.l-back:not(.s-status-editing) {
.l-back {
margin-right: $interiorMarginLg;
&.s-status-editing { display: none; }
}
}
}

View File

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

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: timelineController.width(zoomController),
fullWidth: zoomController.width(timelineController.end()),
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: timelineController.width(zoomController) + 'px' }">
ng-style="{ width: zoomController.width(timelineController.end()) + '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: timelineController.width(zoomController) + 'px' }">
ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
</div>
</div>
</div>

View File

@ -27,11 +27,15 @@ 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) {
function CompositionColumn(index, idMap) {
this.index = index;
this.idMap = idMap;
}
CompositionColumn.prototype.name = function () {
@ -41,7 +45,9 @@ define([], function () {
CompositionColumn.prototype.value = function (domainObject) {
var model = domainObject.getModel(),
composition = model.composition || [];
return (composition[this.index]) || "";
return composition.length > this.index ?
this.idMap[composition[this.index]] : "";
};
return CompositionColumn;

View File

@ -27,14 +27,23 @@ 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(exportService, notificationService, context) {
function ExportTimelineAsCSVAction(
$log,
exportService,
notificationService,
resources,
context
) {
this.$log = $log;
this.task = new ExportTimelineAsCSVTask(
exportService,
resources,
context.domainObject
);
this.notificationService = notificationService;
@ -45,13 +54,15 @@ 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 () {
.catch(function (err) {
$log.warn(err);
notification.dismiss();
notificationService.error("Error exporting CSV");
});

View File

@ -35,11 +35,13 @@ 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, domainObject) {
function ExportTimelineAsCSVTask(exportService, resources, domainObject) {
this.domainObject = domainObject;
this.exportService = exportService;
this.resources = resources;
}
/**
@ -50,9 +52,10 @@ define([
*/
ExportTimelineAsCSVTask.prototype.run = function () {
var exportService = this.exportService;
var resources = this.resources;
function doExport(objects) {
var exporter = new TimelineColumnizer(objects),
var exporter = new TimelineColumnizer(objects, resources),
options = { headers: exporter.headers() };
return exporter.rows().then(function (rows) {
return exportService.exportCSV(rows, options);

View File

@ -23,19 +23,23 @@
define([], function () {
/**
* A column showing domain object identifiers.
* A column showing identifying domain objects.
* @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() {
function IdColumn(idMap) {
this.idMap = idMap;
}
IdColumn.prototype.name = function () {
return "Identifier";
return "Index";
};
IdColumn.prototype.value = function (domainObject) {
return domainObject.getId();
return this.idMap[domainObject.getId()];
};
return IdColumn;

View File

@ -27,10 +27,14 @@ 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) {
function ModeColumn(index, idMap) {
this.index = index;
this.idMap = idMap;
}
ModeColumn.prototype.name = function () {
@ -39,8 +43,9 @@ define([], function () {
ModeColumn.prototype.value = function (domainObject) {
var model = domainObject.getModel(),
composition = (model.relationships || {}).modes || [];
return (composition[this.index]) || "";
modes = (model.relationships || {}).modes || [];
return modes.length > this.index ?
this.idMap[modes[this.index]] : "";
};
return ModeColumn;

View File

@ -25,13 +25,15 @@ define([
"./ModeColumn",
"./CompositionColumn",
"./MetadataColumn",
"./TimespanColumn"
"./TimespanColumn",
"./UtilizationColumn"
], function (
IdColumn,
ModeColumn,
CompositionColumn,
MetadataColumn,
TimespanColumn
TimespanColumn,
UtilizationColumn
) {
/**
@ -63,15 +65,17 @@ 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) {
function TimelineColumnizer(domainObjects, resources) {
var maxComposition = 0,
maxRelationships = 0,
columnNames = {},
columns = [],
foundTimespan = false,
idMap,
i;
function addMetadataProperty(property) {
@ -82,7 +86,12 @@ define([
}
}
columns.push(new IdColumn());
idMap = domainObjects.reduce(function (map, domainObject, index) {
map[domainObject.getId()] = index + 1;
return map;
}, {});
columns.push(new IdColumn(idMap));
domainObjects.forEach(function (domainObject) {
var model = domainObject.getModel(),
@ -113,12 +122,16 @@ 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));
columns.push(new CompositionColumn(i, idMap));
}
for (i = 0; i < maxRelationships; i += 1) {
columns.push(new ModeColumn(i));
columns.push(new ModeColumn(i, idMap));
}
this.domainObjects = domainObjects;

View File

@ -0,0 +1,72 @@
/*****************************************************************************
* 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,6 +193,13 @@ 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,15 +79,6 @@ 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) {
@ -121,10 +112,10 @@ define(
// Expose active set of swimlanes
return {
/**
* Get the width, in pixels, of the timeline area
* @returns {number} width, in pixels
* Get the end of the displayed timeline, in milliseconds.
* @returns {number} the end of the displayed timeline
*/
width: width,
end: swimlanePopulator.end.bind(swimlanePopulator),
/**
* Get the swimlanes which should currently be displayed.
* @returns {TimelineSwimlane[]} the swimlanes

View File

@ -22,27 +22,17 @@
define(
[],
function () {
var PADDING = 0.25;
/**
* Controls the pan-zoom state of a timeline view.
* @constructor
*/
function TimelineZoomController($scope, ZOOM_CONFIGURATION) {
function TimelineZoomController($scope, $timeout, 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,
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;
}
tickWidth = ZOOM_CONFIGURATION.width || 200;
function toMillis(pixels) {
return (pixels / tickWidth) * zoomLevels[zoomIndex];
@ -63,14 +53,20 @@ define(
}
}
function setScroll(x) {
$timeout(function () {
$scope.scroll.x = x;
}, 0);
}
function initializeZoomFromTimespan(timespan) {
var timelineDuration = timespan.getDuration();
zoomIndex = 0;
while (toMillis(bounds.width) < timelineDuration &&
while (toMillis($scope.scroll.width) < timelineDuration &&
zoomIndex < zoomLevels.length - 1) {
zoomIndex += 1;
}
bounds.x = toPixels(timespan.getStart());
setScroll(toPixels(timespan.getStart()));
}
function initializeZoom() {
@ -80,9 +76,6 @@ define(
}
}
$scope.$watch("scroll", function (scroll) {
bounds = scroll;
});
$scope.$watch("domainObject", initializeZoom);
return {
@ -100,9 +93,10 @@ 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);
bounds.x = this.toPixels(center) - bounds.width / 2;
setScroll(this.toPixels(center) - bounds.width / 2);
}
return zoomLevels[zoomIndex];
},
@ -124,16 +118,14 @@ define(
*/
toMillis: toMillis,
/**
* 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
* 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
*/
duration: function (value) {
if (arguments.length > 0) {
duration = roundDuration(value);
}
return duration;
width: function (timestamp) {
var pixels = Math.ceil(toPixels(timestamp * (1 + PADDING)));
return Math.max($scope.scroll.width, pixels);
}
};
}

View File

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

View File

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

View File

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

View File

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

View File

@ -23,13 +23,20 @@
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;
column = new ModeColumn(testIndex);
testIdMap = TEST_IDS.reduce(function (map, id, index) {
map[id] = index;
return map;
}, {});
column = new ModeColumn(testIndex, testIdMap);
});
it("includes a one-based index in its name", function () {
@ -48,15 +55,15 @@ define(
);
testModel = {
relationships: {
modes: ['a', 'b', 'c', 'd', 'e', 'f']
modes: TEST_IDS
}
};
mockDomainObject.getModel.andReturn(testModel);
});
it("returns a corresponding identifier", function () {
it("returns a corresponding value from the map", function () {
expect(column.value(mockDomainObject))
.toEqual(testModel.relationships.modes[testIndex]);
.toEqual(testIdMap[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,13 +94,6 @@ 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,23 +214,6 @@ 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,6 +28,7 @@ define(
describe("The timeline zoom state controller", function () {
var testConfiguration,
mockScope,
mockTimeout,
controller;
beforeEach(function () {
@ -37,8 +38,11 @@ define(
};
mockScope = jasmine.createSpyObj("$scope", ['$watch']);
mockScope.commit = jasmine.createSpy('commit');
mockScope.scroll = { x: 0, width: 1000 };
mockTimeout = jasmine.createSpy('$timeout');
controller = new TimelineZoomController(
mockScope,
mockTimeout,
testConfiguration
);
});
@ -47,12 +51,6 @@ 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);
@ -70,11 +68,6 @@ 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,
@ -115,6 +108,10 @@ define(
mockScope.$watch.calls.forEach(function (call) {
call.args[1](mockScope[call.args[0]]);
});
mockTimeout.calls.forEach(function (call) {
call.args[0]();
});
});
it("zooms to fit the timeline", function () {
@ -125,6 +122,27 @@ 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));
});
});
});