Compare commits

..

70 Commits

Author SHA1 Message Date
d475d767d5 add grootprovider 2016-06-17 17:05:05 -07:00
a63e053399 [ObjectAPI] Draft new Object API
Rought prototype of new object API.
2016-06-17 16:59:35 -07:00
5de7a96ccc Merge pull request #1010 from nasa/api-type-proto
[API Prototype] Type registration
2016-06-17 10:18:42 -07:00
09a833f524 Merge branch 'api-tutorials' into api-type-proto 2016-06-10 13:28:09 -07:00
9c4e17bfab [Tutorials] Add telemetry tutorial 2016-06-07 13:14:36 -07:00
d3e5d95d6b [Tutorials] Add example server 2016-06-07 13:00:38 -07:00
c70793ac2d [Tutorials] Add remainder of bargraph 2016-06-07 12:55:29 -07:00
a6ef1d3423 [Tutorials] Add Bar Graph tutorial 2016-06-07 12:49:38 -07:00
c4fec1af6a [API] Move type toward a newer API 2016-05-27 13:31:30 -07:00
a6996df3df [API] Begin moving out type 2016-05-27 13:17:16 -07:00
0c660238f2 [API] Add MCT class 2016-05-27 11:49:43 -07:00
b73b824e55 [API] Add EventEmitter dep 2016-05-27 11:45:59 -07:00
1954d98628 [Tutorials] Remove diff markings in TodoController 2016-05-27 11:30:53 -07:00
7aa034ce23 Add todo tutorial 2016-05-26 16:05:38 -07:00
385dc5d298 Begin adding tutorials 2016-05-26 15:36:09 -07:00
a88b4b31a1 Merge pull request #966 from nasa/open747
R&I open747: refined "unsaved changes" dialog message
2016-05-26 12:02:12 -07:00
04112956cf Merge pull request #964 from nasa/open913
R&I open913: various Timeline fixes
2016-05-26 11:47:06 -07:00
f1113fda24 [Frontend] New message for unsaved changes warning
open #747
2016-05-26 09:04:40 -07:00
33c208d8fe [Frontend] Timeline Gantt bar mods to allow small min-width
open #965
- CSS adjusted to handle min-width of 2px and better
approach to ellipsizing text;
- Angular ng-class added to hide icon and title if
width less than a value;
- Rounded corners on bars removed;
2016-05-25 20:52:36 -07:00
c557fb6cd5 [Frontend] Cursor properties modified
open #768
2016-05-25 19:39:56 -07:00
bde2bc7709 [Frontend] Bottom of holder divs adjusted
open #913
2016-05-25 19:28:28 -07:00
5fe759aa91 Merge pull request #955 from nasa/timeline-zoom-936
[Timeline] Improve zoom behaviors
2016-05-25 16:58:20 -07:00
a5b7badb95 [Timeline] Remove obsolete arguments
https://github.com/nasa/openmct/pull/955/files#r64668507
2016-05-25 16:08:28 -07:00
eefd4c8669 Merge pull request #949 from nasa/info-error-948
[Mobile] Remove usage of element.scope()
2016-05-25 15:15:53 -07:00
7c11f2db4f Merge pull request #961 from nasa/open959
[Style] Fixed style issues introduced by #954
2016-05-25 12:16:44 -07:00
7501f679f7 [Style] Fixed style issues introduced by #954 2016-05-25 12:10:39 -07:00
52e087d8f8 Merge pull request #954 from nasa/open628
[Edit Mode] #628 Remove edit related concerns from Create Action
2016-05-25 11:55:17 -07:00
c5cd495fce Merge pull request #952 from nasa/fix-build-142
[Code Style] Run code style checks on CircleCI
2016-05-25 11:49:54 -07:00
37a417051d Merge remote-tracking branch 'origin/missing-time-conductor-957' 2016-05-25 11:43:57 -07:00
97d819739c Merge pull request #956 from nasa/fix-build-mct-popup
Resolve build conflict from #922
2016-05-25 11:41:14 -07:00
3935378b0c Revert "[Timeline] Test mct-representation ordering"
This reverts commit 2a4004fd5b.
2016-05-25 11:34:29 -07:00
952f95aa4c [Timeline] Update failing specs 2016-05-25 11:33:51 -07:00
0a75a5be1f [Timeline] Add minimal test case 2016-05-25 11:28:00 -07:00
70b593e28a [Timeline] Watch for configuration object
...to address #908 in a manner which does not cause #957
2016-05-25 11:22:22 -07:00
ed69a65f9b [Representation] Restore ordering in mct-representation
Revert "[Timeline] Change ordering in mct-representation"

This reverts commit 20ecf168f2.
These changes introduced a regression due to ordering
expected by time conductor, #957
2016-05-25 11:03:32 -07:00
05b4f5401e Merge remote-tracking branch 'origin/open890' 2016-05-25 10:19:11 -07:00
2ff0c7b06a [Test] Add spy method for addClass
Add spy method, fix a merge conflict that was improperly resolved
in https://github.com/nasa/openmct/pull/922
2016-05-25 10:11:34 -07:00
2330f1d135 Merge remote-tracking branch 'origin/open907' 2016-05-25 10:06:55 -07:00
ff92d3acab Merge remote-tracking branch 'origin/open889' into open922 2016-05-25 09:55:09 -07:00
32d7187db6 Relocated creation package to edit bundle 2016-05-24 17:08:12 -07:00
00534f8af7 [Timeline] Account for tick size
Account for tick size in duration reported by TimelineZoomController,
to avoid tick marks being cut off prematurely due to changes for
#936
2016-05-24 13:02:30 -07:00
3795570938 [Timeline] Rename shadowing variable 2016-05-24 12:45:25 -07:00
362248a02e [Timeline] Run gulp fixstyle 2016-05-24 12:37:10 -07:00
16d20eabd2 [Timeline] Simplify method 2016-05-24 12:33:47 -07:00
eb5566f041 [Timeline] Add tests for timeline zoom changes 2016-05-24 12:33:19 -07:00
757da1dff4 [Timeline] Remove obsolete test cases 2016-05-24 11:54:49 -07:00
85432af187 [Timeline] Don't store zoom configuration
https://github.com/nasa/openmct/issues/936#issuecomment-221343620
2016-05-24 11:53:27 -07:00
f0ab817e87 Added tests, and fixed failing ones 2016-05-24 10:53:04 -07:00
96af931c0b Modified EditActionPolicy to prevent editing of table views unless object is a table type 2016-05-23 16:48:31 -07:00
9a5209f7c2 [Timeline] Add zoom-to-fit button 2016-05-23 16:06:10 -07:00
0818a7cda0 [Timeline] Increase maximum zoom level
#936
2016-05-23 15:36:26 -07:00
f35947361c [Timeline] Remain centered during zoom
#936
2016-05-23 15:32:48 -07:00
9a8bcc0550 [Code Style] Specify lint, codestyle in CircleCI config
...to work around unexpected failure running test suite via gulp verify,
https://circleci.com/gh/nasa/openmct/1981
2016-05-23 15:10:05 -07:00
eff46b076c [New Edit Mode] #628 Removed duplicate logic from Create Action 2016-05-23 15:03:20 -07:00
ab64b682c3 [Code Style] Run checkstyle on CircleCI
Run the full gulp verify task for testing on CircleCI, to handle
unit tests as well as code style checks and linting (and other
verification steps that may be added in the future.)
2016-05-23 14:58:23 -07:00
1c007ea256 [Code Style] Remove trailing whitespace
...to fix build after changes for #142.
2016-05-23 14:55:04 -07:00
6c1412784b [Example] REMS heirarchy appear as links 2016-05-20 17:11:07 -07:00
6e7f4df5e3 [Mobile] Remove usage of element.scope()
Usage is unnecessary and is sensitive to initialization ordering of
representations, resulting in #948.
2016-05-20 16:13:51 -07:00
5eff4e45c9 [Tutorials] #907 Updated tutorials to use new bundle registration mechanism 2016-05-17 17:24:32 -07:00
70a13a75d5 Updating tutorials 2016-05-17 13:22:15 -07:00
69cc1086df Merge pull request #924 from nasa/open889b
[Popup] Add method to jqlite mocks
2016-05-16 10:14:52 -07:00
3d891073da [Popup] Add method to jqlite mocks
...to resolve build failure preventing merge,
https://github.com/nasa/openmct/pull/922#issuecomment-218588876
2016-05-11 14:25:23 -07:00
73c2c01def [Frontend] Layout and styling of Time Conductor
open #889
open #298
Fixes for mobile;
Moved popup z-index def into sass;
Datetime picker compressed for
better fit in mobile phone;
Removed hours selector from datetime
picker to enable better fit in mobile;
2016-05-11 11:32:39 -07:00
7c82e31b66 Merge branch 'master' into open889 2016-05-10 23:09:59 -07:00
2829b8d495 [Frontend] Time Conductor mobile
open #889
- Major refactoring of mobile approach,
leveraging flex-box layout change;
2016-05-10 19:52:11 -07:00
7ade873365 [Frontend] Flex; Tweaks to slider knobs
open #889
- Converted TC elements to use flex
layout instead of abs pos;
- Knob size increased;
- Knob grippies added;
2016-05-10 15:12:50 -07:00
8788523c25 [Frontend] Tweaks to slider knobs
open #889
- Knob size increased;
- Knob grippies added;
2016-05-10 14:28:23 -07:00
c301523156 [Frontend] Layout and positioning fixes for TC controls
open #889
IN PROGRESS
- Smaller font used on range value;
- More space allocated to left and right
for slider range values;
- Style tweaks to slider look;
- Layout and style of datetime inputs
fixed;
- Input error colors fixed, moved to
theme constants;
2016-05-09 17:56:50 -07:00
2e8604e18d [Frontend] Layout and positioning fixes for TC controls
open #889
2016-05-09 10:38:18 -07:00
cae85f3e30 [Frontend] Mods to slider .range-value elems
open #889
- Text smaller, line breaks;
- Height adjustments
- Increased with of slider area;
2016-05-09 10:16:54 -07:00
95 changed files with 2661 additions and 1604 deletions

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

@ -17,6 +17,7 @@ deployment:
test:
post:
- gulp lint
- gulp checkstyle
general:
branches:

File diff suppressed because it is too large Load Diff

View File

@ -45,11 +45,12 @@ define(
function buildTaxonomy(dictionary){
var models = {};
function addMeasurement(measurement){
function addMeasurement(measurement, parent){
var format = FORMAT_MAPPINGS[measurement.type];
models[makeId(measurement)] = {
type: "msl.measurement",
name: measurement.name,
location: parent,
telemetry: {
key: measurement.identifier,
ranges: [{
@ -62,17 +63,24 @@ define(
};
}
function addInstrument(subsystem) {
var measurements = (subsystem.measurements || []);
models[makeId(subsystem)] = {
function addInstrument(subsystem, spacecraftId) {
var measurements = (subsystem.measurements || []),
instrumentId = makeId(subsystem);
models[instrumentId] = {
type: "msl.instrument",
name: subsystem.name,
location: spacecraftId,
composition: measurements.map(makeId)
};
measurements.forEach(addMeasurement);
measurements.forEach(function(measurement) {
addMeasurement(measurement, instrumentId);
});
}
(dictionary.instruments || []).forEach(addInstrument);
(dictionary.instruments || []).forEach(function(instrument) {
addInstrument(instrument, "msl:curiosity");
});
return models;
}

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

@ -24,23 +24,14 @@ define([
"./src/BrowseController",
"./src/PaneController",
"./src/BrowseObjectController",
"./src/creation/CreateMenuController",
"./src/creation/LocatorController",
"./src/MenuArrowController",
"./src/navigation/NavigationService",
"./src/creation/CreationPolicy",
"./src/navigation/NavigateAction",
"./src/windowing/NewTabAction",
"./src/windowing/FullscreenAction",
"./src/creation/CreateActionProvider",
"./src/creation/AddActionProvider",
"./src/creation/CreationService",
"./src/windowing/WindowTitler",
"text!./res/templates/browse.html",
"text!./res/templates/create/locator.html",
"text!./res/templates/browse-object.html",
"text!./res/templates/create/create-button.html",
"text!./res/templates/create/create-menu.html",
"text!./res/templates/items/grid-item.html",
"text!./res/templates/browse/object-header.html",
"text!./res/templates/menu-arrow.html",
@ -53,23 +44,14 @@ define([
BrowseController,
PaneController,
BrowseObjectController,
CreateMenuController,
LocatorController,
MenuArrowController,
NavigationService,
CreationPolicy,
NavigateAction,
NewTabAction,
FullscreenAction,
CreateActionProvider,
AddActionProvider,
CreationService,
WindowTitler,
browseTemplate,
locatorTemplate,
browseObjectTemplate,
createButtonTemplate,
createMenuTemplate,
gridItemTemplate,
objectHeaderTemplate,
menuArrowTemplate,
@ -136,22 +118,6 @@ define([
"$route"
]
},
{
"key": "CreateMenuController",
"implementation": CreateMenuController,
"depends": [
"$scope"
]
},
{
"key": "LocatorController",
"implementation": LocatorController,
"depends": [
"$scope",
"$timeout",
"objectService"
]
},
{
"key": "MenuArrowController",
"implementation": MenuArrowController,
@ -160,12 +126,6 @@ define([
]
}
],
"controls": [
{
"key": "locator",
"template": locatorTemplate
}
],
"representations": [
{
"key": "view-object",
@ -181,17 +141,6 @@ define([
"view"
]
},
{
"key": "create-button",
"template": createButtonTemplate
},
{
"key": "create-menu",
"template": createMenuTemplate,
"uses": [
"action"
]
},
{
"key": "grid-item",
"template": gridItemTemplate,
@ -244,12 +193,6 @@ define([
"implementation": NavigationService
}
],
"policies": [
{
"implementation": CreationPolicy,
"category": "creation"
}
],
"actions": [
{
"key": "navigate",
@ -302,42 +245,6 @@ define([
"editable": false
}
],
"components": [
{
"key": "CreateActionProvider",
"provides": "actionService",
"type": "provider",
"implementation": CreateActionProvider,
"depends": [
"$q",
"typeService",
"navigationService",
"policyService"
]
},
{
"key": "AddActionProvider",
"provides": "actionService",
"type": "provider",
"implementation": AddActionProvider,
"depends": [
"$q",
"typeService",
"dialogService",
"policyService"
]
},
{
"key": "CreationService",
"provides": "creationService",
"type": "provider",
"implementation": CreationService,
"depends": [
"$q",
"$log"
]
}
],
"runs": [
{
"implementation": WindowTitler,

View File

@ -1,130 +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.
*****************************************************************************/
/**
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../../src/creation/CreateAction"],
function (CreateAction) {
describe("The create action", function () {
var mockType,
mockParent,
mockContext,
mockDialogService,
mockCreationService,
action;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
mockType = jasmine.createSpyObj(
"type",
[
"getKey",
"getGlyph",
"getName",
"getDescription",
"getProperties",
"getInitialModel"
]
);
mockParent = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getModel",
"getCapability"
]
);
mockContext = {
domainObject: mockParent
};
mockDialogService = jasmine.createSpyObj(
"dialogService",
["getUserInput"]
);
mockCreationService = jasmine.createSpyObj(
"creationService",
["createObject"]
);
mockType.getKey.andReturn("test");
mockType.getGlyph.andReturn("T");
mockType.getDescription.andReturn("a test type");
mockType.getName.andReturn("Test");
mockType.getProperties.andReturn([]);
mockType.getInitialModel.andReturn({});
mockDialogService.getUserInput.andReturn(mockPromise({}));
action = new CreateAction(
mockType,
mockParent,
mockContext,
mockDialogService,
mockCreationService
);
});
it("exposes type-appropriate metadata", function () {
var metadata = action.getMetadata();
expect(metadata.name).toEqual("Test");
expect(metadata.description).toEqual("a test type");
expect(metadata.glyph).toEqual("T");
});
//TODO: Disabled for NEM Beta
xit("invokes the creation service when performed", function () {
action.perform();
expect(mockCreationService.createObject).toHaveBeenCalledWith(
{ type: "test" },
mockParent
);
});
//TODO: Disabled for NEM Beta
xit("does not create an object if the user cancels", function () {
mockDialogService.getUserInput.andReturn({
then: function (callback, fail) {
fail();
}
});
action.perform();
expect(mockCreationService.createObject)
.not.toHaveBeenCalled();
});
});
}
);

View File

@ -43,6 +43,15 @@ define([
"./src/capabilities/EditorCapability",
"./src/capabilities/TransactionCapabilityDecorator",
"./src/services/TransactionService",
"./src/creation/CreateMenuController",
"./src/creation/LocatorController",
"./src/creation/CreationPolicy",
"./src/creation/CreateActionProvider",
"./src/creation/AddActionProvider",
"./src/creation/CreationService",
"text!./res/templates/create/locator.html",
"text!./res/templates/create/create-button.html",
"text!./res/templates/create/create-menu.html",
"text!./res/templates/library.html",
"text!./res/templates/edit-object.html",
"text!./res/templates/edit-action-buttons.html",
@ -72,6 +81,15 @@ define([
EditorCapability,
TransactionCapabilityDecorator,
TransactionService,
CreateMenuController,
LocatorController,
CreationPolicy,
CreateActionProvider,
AddActionProvider,
CreationService,
locatorTemplate,
createButtonTemplate,
createMenuTemplate,
libraryTemplate,
editObjectTemplate,
editActionButtonsTemplate,
@ -112,6 +130,22 @@ define([
"$location",
"policyService"
]
},
{
"key": "CreateMenuController",
"implementation": CreateMenuController,
"depends": [
"$scope"
]
},
{
"key": "LocatorController",
"implementation": LocatorController,
"depends": [
"$scope",
"$timeout",
"objectService"
]
}
],
"directives": [
@ -219,10 +253,13 @@ define([
},
{
"category": "navigation",
"message": "There are unsaved changes.",
"message": "Continuing will cause the loss of any unsaved changes.",
"implementation": EditNavigationPolicy
},
{
"implementation": CreationPolicy,
"category": "creation"
}
],
"templates": [
{
@ -261,6 +298,17 @@ define([
{
"key": "topbar-edit",
"template": topbarEditTemplate
},
{
"key": "create-button",
"template": createButtonTemplate
},
{
"key": "create-menu",
"template": createMenuTemplate,
"uses": [
"action"
]
}
],
"components": [
@ -282,7 +330,40 @@ define([
"$q",
"$log"
]
},
{
"key": "CreateActionProvider",
"provides": "actionService",
"type": "provider",
"implementation": CreateActionProvider,
"depends": [
"typeService",
"policyService"
]
},
{
"key": "AddActionProvider",
"provides": "actionService",
"type": "provider",
"implementation": AddActionProvider,
"depends": [
"$q",
"typeService",
"dialogService",
"policyService"
]
},
{
"key": "CreationService",
"provides": "creationService",
"type": "provider",
"implementation": CreationService,
"depends": [
"$q",
"$log"
]
}
],
"representers": [
{
@ -316,6 +397,12 @@ define([
"transactionService"
]
}
],
"controls": [
{
"key": "locator",
"template": locatorTemplate
}
]
}
});

View File

@ -74,6 +74,12 @@ define(
self.domainObject.getCapability('editor').cancel();
self.navigationService.removeListener(cancelEditing);
}
//If this is not the currently navigated object, then navigate
// to it.
if (this.navigationService.getNavigation() !== this.domainObject) {
this.navigationService.setNavigation(this.domainObject);
}
this.navigationService.addListener(cancelEditing);
this.domainObject.useCapability("editor");
};

View File

@ -22,7 +22,7 @@
define(
['../../../browse/src/creation/CreateWizard'],
['../creation/CreateWizard'],
function (CreateWizard) {
/**

View File

@ -43,11 +43,8 @@ define(
* override this)
* @param {ActionContext} context the context in which the
* action is being performed
* @param {NavigationService} navigationService the navigation service,
* which handles changes in navigation. It allows the object
* being browsed/edited to be set.
*/
function CreateAction(type, parent, context, $q, navigationService) {
function CreateAction(type, parent, context) {
this.metadata = {
key: 'create',
glyph: type.getGlyph(),
@ -56,24 +53,8 @@ define(
description: type.getDescription(),
context: context
};
this.type = type;
this.parent = parent;
this.navigationService = navigationService;
this.$q = $q;
}
// Get a count of views which are not flagged as non-editable.
function countEditableViews(domainObject) {
var views = domainObject && domainObject.useCapability('view'),
count = 0;
// A view is editable unless explicitly flagged as not
(views || []).forEach(function (view) {
count += (view.editable !== false) ? 1 : 0;
});
return count;
}
/**
@ -82,26 +63,31 @@ define(
*/
CreateAction.prototype.perform = function () {
var newModel = this.type.getInitialModel(),
parentObject = this.navigationService.getNavigation(),
editorCapability,
newObject;
newObject,
editAction,
editorCapability;
function onSave() {
return editorCapability.save();
}
function onCancel() {
return editorCapability.cancel();
}
newModel.type = this.type.getKey();
newModel.location = parentObject.getId();
newObject = parentObject.useCapability('instantiation', newModel);
newModel.location = this.parent.getId();
newObject = this.parent.useCapability('instantiation', newModel);
editorCapability = newObject.hasCapability('editor') && newObject.getCapability("editor");
editorCapability = newObject.getCapability("editor");
if (countEditableViews(newObject) > 0 && newObject.hasCapability('composition')) {
this.navigationService.setNavigation(newObject);
return newObject.getCapability("action").perform("edit");
} else {
editAction = newObject.getCapability("action").getActions("edit")[0];
//If an edit action is available, perform it
if (editAction) {
return editAction.perform();
} else if (editorCapability) {
//otherwise, use the save action
editorCapability.edit();
return newObject.useCapability("action").perform("save").then(function () {
return editorCapability.save();
}, function () {
return editorCapability.cancel();
});
return newObject.getCapability("action").perform("save").then(onSave, onCancel);
}
};

View File

@ -44,10 +44,8 @@ define(
* introduced in this bundle), responsible for handling actual
* object creation.
*/
function CreateActionProvider($q, typeService, navigationService, policyService) {
function CreateActionProvider(typeService, policyService) {
this.typeService = typeService;
this.navigationService = navigationService;
this.$q = $q;
this.policyService = policyService;
}
@ -72,9 +70,7 @@ define(
return new CreateAction(
type,
destination,
context,
self.$q,
self.navigationService
context
);
});
};

View File

@ -56,7 +56,10 @@ define(
// A view is editable unless explicitly flagged as not
(views || []).forEach(function (view) {
if (view.editable === true ||
(view.key === 'plot' && type.getKey() === 'telemetry.panel')) {
(view.key === 'plot' && type.getKey() === 'telemetry.panel') ||
(view.key === 'table' && type.getKey() === 'table') ||
(view.key === 'rt-table' && type.getKey() === 'rttable')
) {
count++;
}
});

View File

@ -29,13 +29,10 @@ define(
describe("The create action provider", function () {
var mockTypeService,
mockDialogService,
mockNavigationService,
mockPolicyService,
mockCreationPolicy,
mockPolicyMap = {},
mockTypes,
mockQ,
provider;
function createMockType(name) {
@ -61,14 +58,6 @@ define(
"typeService",
["listTypes"]
);
mockDialogService = jasmine.createSpyObj(
"dialogService",
["getUserInput"]
);
mockNavigationService = jasmine.createSpyObj(
"navigationService",
["setNavigation"]
);
mockPolicyService = jasmine.createSpyObj(
"policyService",
["allow"]
@ -91,9 +80,7 @@ define(
mockTypeService.listTypes.andReturn(mockTypes);
provider = new CreateActionProvider(
mockQ,
mockTypeService,
mockNavigationService,
mockPolicyService
);
});

View File

@ -0,0 +1,190 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/**
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../../src/creation/CreateAction"],
function (CreateAction) {
describe("The create action", function () {
var mockType,
mockParent,
mockContext,
mockDomainObject,
capabilities = {},
mockEditAction,
mockSaveAction,
action;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
mockType = jasmine.createSpyObj(
"type",
[
"getKey",
"getGlyph",
"getName",
"getDescription",
"getProperties",
"getInitialModel"
]
);
mockParent = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getModel",
"getCapability",
"useCapability"
]
);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getModel",
"getCapability",
"hasCapability",
"useCapability"
]
);
mockDomainObject.hasCapability.andCallFake(function (name) {
return !!capabilities[name];
});
mockDomainObject.getCapability.andCallFake(function (name) {
return capabilities[name];
});
mockSaveAction = jasmine.createSpyObj(
"saveAction",
[
"perform"
]
);
capabilities.action = jasmine.createSpyObj(
"actionCapability",
[
"getActions",
"perform"
]
);
capabilities.editor = jasmine.createSpyObj(
"editorCapability",
[
"edit",
"save",
"cancel"
]
);
mockEditAction = jasmine.createSpyObj(
"editAction",
[
"perform"
]
);
mockContext = {
domainObject: mockParent
};
mockParent.useCapability.andReturn(mockDomainObject);
mockType.getKey.andReturn("test");
mockType.getGlyph.andReturn("T");
mockType.getDescription.andReturn("a test type");
mockType.getName.andReturn("Test");
mockType.getProperties.andReturn([]);
mockType.getInitialModel.andReturn({});
action = new CreateAction(
mockType,
mockParent,
mockContext
);
});
it("exposes type-appropriate metadata", function () {
var metadata = action.getMetadata();
expect(metadata.name).toEqual("Test");
expect(metadata.description).toEqual("a test type");
expect(metadata.glyph).toEqual("T");
});
describe("the perform function", function () {
beforeEach(function () {
capabilities.action.getActions.andReturn([mockEditAction]);
});
it("uses the instantiation capability when performed", function () {
action.perform();
expect(mockParent.useCapability).toHaveBeenCalledWith("instantiation", jasmine.any(Object));
});
it("uses the edit action if available", function () {
action.perform();
expect(mockEditAction.perform).toHaveBeenCalled();
});
it("uses the save action if object does not have an edit action" +
" available", function () {
capabilities.action.getActions.andReturn([]);
capabilities.action.perform.andReturn(mockPromise(undefined));
action.perform();
expect(capabilities.action.perform).toHaveBeenCalledWith("save");
});
describe("uses to editor capability", function () {
var promise = jasmine.createSpyObj("promise", ["then"]);
beforeEach(function () {
capabilities.action.getActions.andReturn([]);
capabilities.action.perform.andReturn(promise);
});
it("to save the edit if user saves dialog", function () {
action.perform();
expect(promise.then).toHaveBeenCalled();
promise.then.mostRecentCall.args[0]();
expect(capabilities.editor.save).toHaveBeenCalled();
});
it("to cancel the edit if user cancels dialog", function () {
action.perform();
promise.then.mostRecentCall.args[1]();
expect(capabilities.editor.cancel).toHaveBeenCalled();
});
});
});
});
}
);

View File

@ -145,3 +145,8 @@
.flex-justify-end {
@include justify-content(flex-end);
}
/********************************************* POPUPS */
.t-popup {
z-index: 75;
}

View File

@ -48,7 +48,7 @@ $uePaneMiniTabW: 10px;
$uePaneMiniTabCollapsedW: 11px;
$ueEditLeftPaneW: 75%;
$treeSearchInputBarH: 25px;
$ueTimeControlH: (33px, 20px, 20px);
$ueTimeControlH: (33px, 18px, 20px);
// Panes
$ueBrowseLeftPaneTreeMinW: 150px;
$ueBrowseLeftPaneTreeMaxW: 35%;

View File

@ -63,9 +63,10 @@ input, textarea {
font-family: Helvetica, Arial, sans-serif;
}
input[type="text"] {
input[type="text"],
input[type="search"] {
vertical-align: baseline;
padding: 3px 5px !important;
padding: 3px 5px;
}
h1, h2, h3 {

View File

@ -139,6 +139,17 @@
background-size: $d $d;
}
@mixin bgStripes($c: yellow, $a: 0.1, $bgsize: 5px, $angle: 90deg) {
@include background-image(linear-gradient($angle,
rgba($c, $a) 25%, transparent 25%,
transparent 50%, rgba($c, $a) 50%,
rgba($c, $a) 75%, transparent 75%,
transparent 100%
));
background-repeat: repeat;
background-size: $bgsize $bgsize;
}
@mixin bgVertStripes($c: yellow, $a: 0.1, $d: 40px) {
@include background-image(linear-gradient(-90deg,
rgba($c, $a) 0%, rgba($c, $a) 50%,
@ -322,13 +333,13 @@
color: $fg;
outline: none;
&.error {
background: rgba(red, 0.5);
background-color: $colorFormFieldErrorBg;
color: $colorFormFieldErrorFg;
}
}
@mixin nice-input($bg: $colorInputBg, $fg: $colorInputFg) {
@include input-base($bg, $fg);
padding: 0 $interiorMarginSm;
}
@mixin contextArrow() {

View File

@ -29,7 +29,7 @@
.accordion-head {
$op: 0.2;
border-radius: $basicCr * 0.75;
box-sizing: "border-box";
box-sizing: border-box;
background: rgba($colorBodyFg, $op);
cursor: pointer;
font-size: 0.75em;
@ -396,11 +396,11 @@ input[type="search"] {
left: auto;
}
.knob-l {
@include border-left-radius($sliderKnobW);
@include border-left-radius($sliderKnobR);
cursor: w-resize;
}
.knob-r {
@include border-right-radius($sliderKnobW);
@include border-right-radius($sliderKnobR);
cursor: e-resize;
}
.range {
@ -426,7 +426,6 @@ input[type="search"] {
@include user-select(none);
font-size: 0.8rem;
padding: $interiorMarginLg !important;
width: 230px;
.l-month-year-pager {
$pagerW: 20px;
height: $r1H;
@ -518,6 +517,19 @@ input[type="search"] {
}
}
@include phone {
.l-datetime-picker {
padding: $interiorMargin !important;
}
.l-calendar {
ul.l-cal-row {
li {
padding: 2px $interiorMargin;
}
}
}
}
/******************************************************** TEXTAREA */
textarea {
@include nice-textarea($colorInputBg, $colorInputFg);

View File

@ -10,25 +10,24 @@
$knobHOffset: 0px;
$knobM: ($sliderKnobW + $knobHOffset) * -1;
$rangeValPad: $interiorMargin;
$rangeValOffset: $sliderKnobW;
$timeRangeSliderLROffset: 130px + $sliderKnobW + $rangeValOffset;
$r1H: nth($ueTimeControlH,1);
$rangeValOffset: $sliderKnobW + $interiorMargin;
$timeRangeSliderLROffset: 150px + ($sliderKnobW * 2);
$r1H: nth($ueTimeControlH,1); // Not currently used
$r2H: nth($ueTimeControlH,2);
$r3H: nth($ueTimeControlH,3);
display: block;
height: $r1H + $r2H + $r3H + ($interiorMargin * 2);
min-width: $minW;
font-size: 0.8rem;
.l-time-range-inputs-holder,
.l-time-range-slider-holder,
.l-time-range-ticks-holder
{
@include absPosDefault(0, visible);
box-sizing: border-box;
top: auto;
position: relative;
&:not(:first-child) {
margin-top: $interiorMargin;
}
}
.l-time-range-slider,
.l-time-range-ticks {
@ -37,14 +36,21 @@
}
.l-time-range-inputs-holder {
height: $r1H; bottom: $r2H + $r3H + ($interiorMarginSm * 2);
padding-top: $interiorMargin;
border-top: 1px solid $colorInteriorBorder;
padding-top: $interiorMargin;
&.l-flex-row,
.l-flex-row {
@include align-items(center);
.flex-elem {
height: auto;
line-height: normal;
}
}
.type-icon {
font-size: 120%;
vertical-align: middle;
}
.l-time-range-input,
.l-time-range-input-w,
.l-time-range-inputs-elem {
margin-right: $interiorMargin;
.lbl {
@ -52,13 +58,27 @@
}
.ui-symbol.icon {
font-size: 11px;
width: 11px;
}
}
.l-time-range-input-w {
// Wraps a datetime text input field
position: relative;
input[type="text"] {
width: 200px;
&.picker-icon {
padding-right: 20px;
}
}
.icon-calendar {
position: absolute;
right: 5px;
top: 5px;
}
}
}
.l-time-range-slider-holder {
height: $r2H; bottom: $r3H + ($interiorMarginSm * 1);
height: $r2H;
.range-holder {
box-shadow: none;
background: none;
@ -73,24 +93,13 @@
width: $myW;
height: auto;
z-index: 2;
&:before,
&:after {
background-color: $myC;
content: "";
position: absolute;
}
&:before {
// Vert line
background-color: $myC;
position: absolute;
content: "";
top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1;
width: 2px;
}
&:after {
// Circle element
border-radius: $myW;
@include transform(translateY(-50%));
top: 50%; right: 0; bottom: auto; left: 0;
width: auto;
height: $myW;
width: 1px;
}
}
&:hover .toi-line {
@ -126,9 +135,9 @@
@include webkitProp(transform, translateX(-50%));
color: $colorPlotLabelFg;
display: inline-block;
font-size: 0.9em;
font-size: 0.7rem;
position: absolute;
top: 8px;
top: 5px;
white-space: nowrap;
z-index: 2;
}
@ -138,16 +147,29 @@
.knob {
z-index: 2;
&:before {
$mTB: 2px;
$grippyW: 3px;
$mLR: ($sliderKnobW - $grippyW)/2;
@include bgStripes($c: pullForward($sliderColorKnob, 20%), $a: 1, $bgsize: 4px, $angle: 0deg);
content: '';
display: block;
position: absolute;
top: $mTB; right: $mLR; bottom: $mTB; left: $mLR;
}
.range-value {
@include trans-prop-nice-fade(.25s);
padding: 0 $rangeValOffset;
font-size: 0.7rem;
position: absolute;
height: $r2H;
line-height: $r2H;
white-space: nowrap;
white-space: nowrap;
z-index: 1;
}
&:hover .range-value {
color: $sliderColorKnobHov;
&:hover {
.range-value {
color: $sliderColorKnobHov;
}
}
&.knob-l {
margin-left: $knobM;
@ -170,7 +192,7 @@
.l-time-domain-selector {
position: absolute;
right: 0px;
bottom: 46px;
top: $interiorMargin;
}
}
@ -181,174 +203,64 @@
padding: 1px 1px 0 $interiorMargin;
}
/******************************************************************** MOBILE */
@include phoneandtablet {
.l-time-controller, .l-time-range-inputs-holder {
min-width: 0px;
}
.l-time-controller {
.l-time-domain-selector {
select {
height: 25px;
margin-bottom: 0px;
}
}
.l-time-range-slider-holder, .l-time-range-ticks-holder {
display: none;
}
.time-range-start, .time-range-end, {
width: 100%;
}
.l-time-range-inputs-holder {
.l-time-range-input {
display: block;
.s-btn {
padding-right: 18px;
white-space: nowrap;
input {
width: 100%;
}
}
}
.l-time-range-inputs-elem {
}
}
}
.l-time-controller {
min-width: 0;
.l-time-range-slider-holder,
.l-time-range-ticks-holder {
display: none;
}
}
}
@include phone {
.l-time-controller {
height: 48px;
.l-time-range-inputs-holder {
bottom: 24px;
}
.l-time-domain-selector {
width: 33%;
bottom: -9px;
}
.l-time-range-inputs-holder {
.l-time-range-input {
margin-bottom: 5px;
.s-btn {
width: 66%;
}
}
.l-time-range-inputs-elem {
&.ui-symbol {
display: none;
}
&.lbl {
width: 33%;
right: 0px;
top: 5px;
display: block;
height: 25px;
margin: 0;
line-height: 25px;
position: absolute;
}
}
}
}
.l-time-controller {
.l-time-range-inputs-holder {
&.l-flex-row,
.l-flex-row {
@include align-items(flex-start);
}
.l-time-range-inputs-elem {
&.type-icon {
margin-top: 3px;
}
}
.t-inputs-w {
@include flex-direction(column);
.l-time-range-input-w:not(:first-child) {
&:not(:first-child) {
margin-top: $interiorMargin;
}
margin-right: 0;
}
.l-time-range-inputs-elem {
&.lbl { display: none; }
}
}
}
}
}
@include tablet {
.l-time-controller {
height: 17px;
.l-time-range-inputs-holder {
bottom: -7px;
left: -5px;
}
.l-time-domain-selector {
width: 23%;
right: -4px;
bottom: -10px;
}
.l-time-range-inputs-holder {
.l-time-range-input {
float: left;
.s-btn {
width: 100%;
padding-left: 4px;
}
}
}
}
}
@include tabletLandscape {
.l-time-controller {
height: 17px;
.l-time-range-inputs-holder {
bottom: -7px;
}
.l-time-domain-selector {
width: 23%;
right: auto;
bottom: -10px;
left: 391px;
}
.l-time-range-inputs-holder {
.l-time-range-inputs-elem {
&.ui-symbol, &.lbl {
display: block;
float: left;
line-height: 25px;
}
}
}
}
.pane-tree-hidden .l-time-controller {
.l-time-domain-selector {
left: 667px;
}
.l-time-range-inputs-holder {
padding-left: 277px;
}
}
}
@include tabletPortrait {
.l-time-controller {
height: 17px;
.l-time-range-inputs-holder {
bottom: -7px;
left: -5px;
}
.l-time-domain-selector {
width: 23%;
right: -4px;
bottom: -10px;
}
.l-time-range-inputs-holder {
.l-time-range-input {
width: 38%;
float: left;
}
.l-time-range-inputs-elem {
&.ui-symbol, &.lbl {
display: none;
}
}
}
}
@include phonePortrait {
.l-time-controller {
.l-time-range-inputs-holder {
.t-inputs-w {
@include flex(1 1 auto);
padding-top: 25px; // Make room for the ever lovin' Time Domain Selector
.flex-elem {
@include flex(1 1 auto);
width: 100%;
}
input[type="text"] {
width: 100%;
}
}
}
}
.l-time-domain-selector {
right: auto;
left: 20px;
}
}

View File

@ -194,7 +194,7 @@ body.desktop .pane .mini-tab-icon.toggle-pane {
.holder.holder-treeview-elements {
top: $bodyMargin;
right: 0;
bottom: $bodyMargin;
bottom: $interiorMargin;
left: $bodyMargin;
.create-btn-holder {
&.s-status-editing {
@ -215,17 +215,17 @@ body.desktop .pane .mini-tab-icon.toggle-pane {
left: 0;
.holder-object {
top: $bodyMargin;
bottom: $bodyMargin;
bottom: $interiorMargin;
}
.holder-inspector {
top: $bodyMargin;
bottom: $bodyMargin;
bottom: $interiorMargin;
left: $bodyMargin;
right: $bodyMargin;
}
.holder-elements {
top: 0;
bottom: $bodyMargin;
bottom: $interiorMargin;
left: $bodyMargin;
right: $bodyMargin;
}

View File

@ -19,18 +19,17 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<span class="s-btn"
ng-controller="DateTimeFieldController">
<span ng-controller="DateTimeFieldController">
<input type="text"
ng-model="textValue"
ng-blur="restoreTextValue(); ngBlur()"
ng-class="{
error: textInvalid ||
(structure.validate &&
!structure.validate(ngModel[field]))
!structure.validate(ngModel[field])),
'picker-icon': structure.format === 'utc' || !structure.format
}">
</input>
<a class="ui-symbol icon icon-calendar"
</input><a class="ui-symbol icon icon-calendar"
ng-if="structure.format === 'utc' || !structure.format"
ng-click="picker.active = !picker.active">
</a>
@ -38,8 +37,7 @@
<div mct-click-elsewhere="picker.active = false">
<mct-control key="'datetime-picker'"
ng-model="pickerModel"
field="'value'"
options="{ hours: true }">
field="'value'">
</mct-control>
</div>
</mct-popup>

View File

@ -19,42 +19,43 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="TimeRangeController as trCtrl">
<form class="l-time-range-inputs-holder"
<div ng-controller="TimeRangeController as trCtrl" class="l-flex-col">
<form class="l-time-range-inputs-holder l-flex-row flex-elem"
ng-submit="trCtrl.updateBoundsFromForm()">
<span class="l-time-range-inputs-elem ui-symbol type-icon">&#x43;</span>
<span class="l-time-range-input">
<mct-control key="'datetime-field'"
structure="{
format: parameters.format,
validate: trCtrl.validateStart
}"
ng-model="formModel"
ng-blur="trCtrl.updateBoundsFromForm()"
field="'start'"
class="time-range-start">
</mct-control>
<span class="l-time-range-inputs-elem ui-symbol type-icon flex-elem">&#x43;</span>
<span class="l-time-range-inputs-elem t-inputs-w l-flex-row flex-elem">
<span class="l-time-range-input-w flex-elem">
<mct-control key="'datetime-field'"
structure="{
format: parameters.format,
validate: trCtrl.validateStart
}"
ng-model="formModel"
ng-blur="trCtrl.updateBoundsFromForm()"
field="'start'"
class="time-range-start">
</mct-control>
</span>
<span class="l-time-range-inputs-elem lbl flex-elem">to</span>
<span class="l-time-range-input-w flex-elem" ng-controller="ToggleController as t2">
<mct-control key="'datetime-field'"
structure="{
format: parameters.format,
validate: trCtrl.validateEnd
}"
ng-model="formModel"
ng-blur="trCtrl.updateBoundsFromForm()"
field="'end'"
class="time-range-end">
</mct-control>
</span>
</span>
<span class="l-time-range-inputs-elem lbl">to</span>
<span class="l-time-range-input" ng-controller="ToggleController as t2">
<mct-control key="'datetime-field'"
structure="{
format: parameters.format,
validate: trCtrl.validateEnd
}"
ng-model="formModel"
ng-blur="trCtrl.updateBoundsFromForm()"
field="'end'"
class="time-range-end">
</mct-control>&nbsp;
</span>
<input type="submit" class="hidden">
</form>
<div class="l-time-range-slider-holder">
<div class="l-time-range-slider-holder flex-elem">
<div class="l-time-range-slider">
<div class="slider"
mct-resize="spanWidth = bounds.width">
@ -85,7 +86,7 @@
</div>
</div>
<div class="l-time-range-ticks-holder">
<div class="l-time-range-ticks-holder flex-elem">
<div class="l-time-range-ticks">
<div
ng-repeat="tick in ticks track by $index"

View File

@ -49,10 +49,7 @@ define(
position = [rect.left, rect.top],
popup = popupService.display(div, position);
// TODO: Handle in CSS;
// https://github.com/nasa/openmctweb/issues/298
div.css('z-index', 75);
div.addClass('t-popup');
transclude(function (clone) {
div.append(clone);
});

View File

@ -24,7 +24,15 @@ define(
["../../src/directives/MCTPopup"],
function (MCTPopup) {
var JQLITE_METHODS = ["on", "off", "find", "parent", "css", "append"];
var JQLITE_METHODS = [
"on",
"off",
"find",
"parent",
"css",
"addClass",
"append"
];
describe("The mct-popup directive", function () {
var mockCompile,

View File

@ -38,7 +38,6 @@ define(
function InfoGestureButton($document, agentService, infoService, element, domainObject) {
var dismissBubble,
touchPosition,
scopeOff,
body = $document.find('body');
function trackPosition(event) {
@ -94,10 +93,6 @@ define(
element.on('click', showBubble);
}
// Also make sure we dismiss bubble if representation is destroyed
// before the mouse actually leaves it
scopeOff = element.scope().$on('$destroy', hideBubble);
return {
/**
* Detach any event handlers associated with this gesture.
@ -109,7 +104,6 @@ define(
hideBubble();
// ...and detach listeners
element.off('click', showBubble);
scopeOff();
}
};
}

View File

@ -137,6 +137,11 @@ define(
);
});
// https://github.com/nasa/openmct/issues/948
it("does not try to access scope", function () {
expect(mockElement.scope).not.toHaveBeenCalled();
});
});
}
);

View File

@ -32,11 +32,12 @@ $sliderColorBase: $colorKey;
$sliderColorRangeHolder: rgba(black, 0.1);
$sliderColorRange: rgba($sliderColorBase, 0.3);
$sliderColorRangeHov: rgba($sliderColorBase, 0.5);
$sliderColorKnob: rgba($sliderColorBase, 0.6);
$sliderColorKnobHov: $sliderColorBase;
$sliderColorKnob: $sliderColorBase;
$sliderColorKnobHov: pullForward($sliderColorKnob, $ltGamma);
$sliderColorRangeValHovBg: rgba($sliderColorBase, 0.1);
$sliderColorRangeValHovFg: $colorKeyFg;
$sliderKnobW: nth($ueTimeControlH,2)/2;
$sliderKnobW: 15px;
$sliderKnobR: 2px;
$timeControllerToiLineColor: #00c2ff;
$timeControllerToiLineColorHov: #fff;
@ -69,8 +70,10 @@ $colorCreateMenuText: $colorMenuFg;
$colorCheck: $colorKey;
$colorFormRequired: $colorAlt1;
$colorFormValid: #33cc33;
$colorFormError: #cc0000;
$colorFormError: #990000;
$colorFormInvalid: #ff3300;
$colorFormFieldErrorBg: $colorFormError;
$colorFormFieldErrorFg: rgba(#fff, 0.6);
$colorFormLines: rgba(#fff, 0.1);
$colorFormSectionHeader: rgba(#fff, 0.1);
$colorInputBg: rgba(#000, 0.1);

View File

@ -32,11 +32,12 @@ $sliderColorBase: $colorKey;
$sliderColorRangeHolder: rgba(black, 0.07);
$sliderColorRange: rgba($sliderColorBase, 0.2);
$sliderColorRangeHov: rgba($sliderColorBase, 0.4);
$sliderColorKnob: rgba($sliderColorBase, 0.5);
$sliderColorKnob: pushBack($sliderColorBase, 20%);
$sliderColorKnobHov: rgba($sliderColorBase, 0.7);
$sliderColorRangeValHovBg: $sliderColorRange; //rgba($sliderColorBase, 0.1);
$sliderColorRangeValHovBg: $sliderColorRange;
$sliderColorRangeValHovFg: $colorBodyFg;
$sliderKnobW: nth($ueTimeControlH,2)/2;
$sliderKnobW: 15px;
$sliderKnobR: 2px;
$timeControllerToiLineColor: $colorBodyFg;
$timeControllerToiLineColorHov: #0052b5;
@ -69,8 +70,10 @@ $colorCreateMenuText: $colorBodyFg;
$colorCheck: $colorKey;
$colorFormRequired: $colorKey;
$colorFormValid: #33cc33;
$colorFormError: #cc0000;
$colorFormError: #990000;
$colorFormInvalid: #ff2200;
$colorFormFieldErrorBg: $colorFormError;
$colorFormFieldErrorFg: rgba(#fff, 0.6);
$colorFormLines: rgba(#000, 0.1);
$colorFormSectionHeader: rgba(#000, 0.05);
$colorInputBg: $colorGenBg;

View File

@ -1,150 +0,0 @@
## Notes
API is notional for now, based on use-cases identified below. Possible the
use cases are not sufficient, so please include in comments
any other use cases you'd like to see.
Plan now is to start building out test suite for the use cases identified below
in order to get the API functional. Need to discuss how UI aspects of timeline will be implemented.
Propose in place refactoring of existing timeline rather than starting again.
Some caveats / open questions
* I don't understand the use case shown on page 52 of UI sketches. It shows RT/FT, with deltas,
with inner interval unlocked. Not sure what result would be, has inner end switched to fixed?
Also example on page 55 in real-time where inner end < now. Is there a use case for this? Semantically, it's saying
show me real time, but stale data. Why would a user want this? My feeling is that if the inner
OR outer ends are moved behind NOW in real-time mode then you drop into historical mode.
* For the API itself, have ignored question of how it's namespaced / exposed.
Examples assume global namespace and availability from window object.
For now API implemented as standard standard Require JS AMDs. Could attach
to window from bundle.js. Perhaps attaching to window not best approach though...
* Have not included validation (eg. start time < end time) or any other
business logic such as what happens when outer interval gets dragged
within range of inner interval. Focus is on teasing out the public API
right now.
* Time systems are vague right now also, I don't know how they're going
to work or whether any API has yet been specified.
* Not clear on the differences between real-time and follow-time as it
concerns the time conductor? For now the API has an end bounds mode
of FOLLOW which automatically tracks current time, and a start time mode
of RELATIVE. I can envision a real-time plot that is not in follow time mode,
but not sure what implication is for time conductor itself and how it
differs from an historical plot?
* Should the time conductor be responsible for choosing time system / domain? Currently
it is.
## Use Cases
1. Historical session is loaded and system sets time bounds on conductor
2. Real-time session is loaded, setting custom start and end deltas
3. User changes time of interest
4. Plot controller listens for change to TOI
5. Plot Controller updated on tick
6. Plot Controller updated when user changes bounds (eg to reset plot zoom)
7. Conductor controller needs to update bounds and mode on TC when user changes bounds
### Additional possible use-cases
1. Telemetry adapter wants to indicate presence of data at a particular time
2. Time conductor controller wants to paint map of data availability.
These use-cases could be features of the TimeConductor, but perhaps makes
sense to make knowledge of data availability the sole preserve of telemetry
adapters, not TimeConductor itself. Adapters will be ultimately responsible
for providing these data so doesn't make much sense to duplicate elsewhere.
The TimeConductorController - which knows tick interval on scale (which
TimeConductor API does not) - could simply request data availability from
telemetry API and paint it into the Time Conductor UI
## Example implementations of use cases
### 1. Real time session is loaded (outside of TC) and system sets time bounds on conductor
``` javascript
function loadSession(telemetryMetadata) {
var tc = MCT.conductor;
tc.timeSystem(session.timeSystem());
//Set start and end modes to fixed date
tc.mode(new FixedMode());
//Set both inner and outer bounds
tc.bounds({start: session.start(), end: session.end()});
}
```
### 2. Real-time session is loaded (outside of TC), setting custom start and end deltas
``` javascript
function loadSession(session) {
var tc = MCT.conductor;
var FIFTEEN_MINUTES = 15 * 60 * 1000;
// Could have a central ticking source somewhere, or connect to a
// remote ticking source. Should not need to be done manually with
// each session load. Actually not quite sure what to do with tick
// sources yet.
var tickSource = new LocalClock();
tickSource.attach(); // Start ticking
var mode = new RealtimeMode({
startDelta: FIFTEEN_MINUTES,
endDelta: 0 // End delta offset is from "Now" in the time system
});
tc.timeSystem(session.timeSystem());
// Set mode to realtime, specifying a tick source
tc.mode(mode);
//No need to set bounds manually, will be established by mode and the deltas specified
}
```
### 3. User changes time of interest
```javascript
//Somewhere in the TimeConductorController...
function changeTOI(newTime) {
MCT.conductor.timeOfInterest(newTime);
}
```
### 4. Plot controller listens for change to TOI
```javascript
// toi is attribute of Time Conductor object. Add a listener to the time
// conductor to be alerted to changes in value
// Time conductor is an event emitter, listen to timeOfInterest event
MCT.conductor.on("timeOfInterest", function (timeOfInterest) {
plot.setTimeOfInterest(timeOfInterest);
}
```
### 5. Plot Controller updated on tick
``` javascript
MCT.conductor.on("bounds", function (bounds) {
plotUpdater.setDomainBounds(bounds.start, bounds.end);
});
```
### 6. Plot Controller updated when user changes bounds (eg to reset plot zoom)
``` javascript
MCT.conductor.on("refresh", function (conductor) {
plot.setBounds(conductor.bounds());
//Also need to reset tick labels. if time system has changed.
}
```
### 7. Conductor controller needs to update bounds and mode on TC when user changes bounds
```javascript
var tc = MCT.conductor;
function dragStartHandle(finalPos){
var bounds = tc.bounds();
bounds.start = positionToTime(finalPos)
tc.bounds(bounds);
}
function dragEndHandle(finalPos){
var bounds = tc.bounds();
bounds.end = positionToTime(finalPos);
tc.bounds(bounds);
}
```

View File

@ -1,148 +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([
"EventEmitter",
"./UTCTimeSystem",
"./modes/RelativeMode",
"./modes/FixedMode"
], function (EventEmitter, UTCTimeSystem, RelativeMode, FixedMode) {
/**
* A class for setting and querying time conductor state.
*
* @event TimeConductor:refresh The time conductor has changed, and its values should be re-queried
* @event TimeConductor:bounds The start time, end time, or both have been updated
* @event TimeConductor:timeOfInterest The Time of Interest has moved.
* @constructor
*/
function TimeConductor() {
EventEmitter.call(this);
//The Time System
this.system = new UTCTimeSystem();
//The Time Of Interest
this.toi = undefined;
this.bounds = {
start: undefined,
end: undefined
};
//Default to fixed mode
this.modeVal = new FixedMode();
}
TimeConductor.prototype = Object.create(EventEmitter.prototype);
/**
* Validate the given bounds. This can be used for pre-validation of
* bounds, for example by views validating user inputs.
* @param bounds The start and end time of the conductor.
* @returns {string | true} A validation error, or true if valid
*/
TimeConductor.prototype.validateBounds = function (bounds) {
if (!bounds.start ||
!bounds.end ||
isNaN(bounds.start) ||
isNaN(bounds.end)
) {
return "Start and end must be specified as integer values";
} else if (bounds.start > bounds.end){
return "Specified start date exceeds end bound";
}
return true;
};
function throwOnError(validationResult) {
if (validationResult !== true) {
throw validationResult;
}
}
/**
* Set the mode of the time conductor.
* @param {FixedMode | RealtimeMode} newMode
* @fires TimeConductor#refresh
* @returns {FixedMode | RealtimeMode}
*/
TimeConductor.prototype.mode = function (newMode) {
if (arguments.length > 0) {
this.modeVal = newMode;
this.emit('refresh', this);
newMode.initialize();
}
return this.modeVal;
};
/**
* @typedef {Object} TimeConductorBounds
* @property {number} start The start time displayed by the time conductor in ms since epoch. Epoch determined by current time system
* @property {number} end The end time displayed by the time conductor in ms since epoch.
*/
/**
* Set the start and end time of the time conductor. Basic validation of bounds is performed.
*
* @param {TimeConductorBounds} newBounds
* @throws {string} Validation error
* @fires TimeConductor#bounds
* @returns {TimeConductorBounds}
*/
TimeConductor.prototype.bounds = function (newBounds) {
if (arguments.length > 0) {
throwOnError(this.validateBounds(newBounds));
this.bounds = newBounds;
this.emit('bounds', this.bounds);
}
return this.bounds;
};
/**
* Set the time system of the TimeConductor. Time systems determine units, epoch, and other aspects of time representation.
* @param newTimeSystem
* @fires TimeConductor#refresh
* @returns {TimeSystem} The currently applied time system
*/
TimeConductor.prototype.timeSystem = function (newTimeSystem) {
if (arguments.length > 0) {
this.system = newTimeSystem;
this.emit('refresh', this);
}
return this.system;
};
/**
* The Time of Interest is the temporal focus of the current view. It can be manipulated by the user from the time
* conductor or from other views.
* @param newTOI
* @returns {*}
*/
TimeConductor.prototype.timeOfInterest = function (newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
this.emit('toi');
}
return this.toi;
};
return TimeConductor;
});

View File

@ -1,69 +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 () {
function TimeConductorBounds(conductor) {
this.listeners = [];
this.start = new TimeConductorLimit(this);
this.end = new TimeConductorLimit(this);
this.conductor = conductor;
}
/**
* @private
*/
TimeConductorBounds.prototype.notify = function (eventType) {
eventType = eventType || this.conductor.EventTypes.EITHER;
this.listeners.forEach(function (element){
if (element.eventType & eventType){
element.listener(this);
}
});
};
/**
* Listen for changes to the bounds
* @param listener a callback function to be called when the bounds change. The bounds object will be passed into
* the function (ie. 'this')
* @param eventType{TimeConductorBounds.EventType} The event type to listen to, ie. system, user, or Both. If not provied, will default to both.
* @returns {Function} an 'unlisten' function
*/
TimeConductorBounds.prototype.listen = function (listener, eventType) {
var self = this,
wrappedListener = {
listener: listener,
eventType: eventType
};
this.listeners.push(wrappedListener);
return function () {
self.listeners = self.listeners.filter(function (element){
return element !== wrappedListener;
});
};
};
return TimeConductorBounds;
});

View File

@ -1,71 +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 () {
/**
* Defines a limit object for a time conductor bounds, ie. start or end values. Holds time and delta values.
*
* TODO: Calculation of time from delta. Should probably be done from the 'tick' function at a higher level,
* which has start and end values in scope to do calculations.
* @param listener
* @constructor
*/
function TimeConductorLimit(listener) {
this.deltaVal = undefined;
this.timeVal = undefined;
this.listener = listener;
}
/**
* Get or set the start value for the bounds. If newVal is provided, will set the start value. May only set delta in
* RELATIVE mode.
* @param {Number} [newVal] a time in ms. A negative value describes a time in the past, positive in the future. A
* start time cannot have a positive delta offset, but an end time can.
* @param {TimeConductor.EventTypes} [eventType=TimeConductor.EventTypes.EITHER] The type of event (User, System, or
* Either)
* @returns {Number} the start date (in milliseconds since some epoch, depending on time system)
*/
TimeConductorLimit.prototype.delta = function (newVal, eventType) {
if (arguments.length > 0) {
this.deltaVal = newVal;
this.listener.notify(eventType);
}
return this.deltaVal;
};
/**
* Get or set the end value for the bounds. If newVal is provided, will set the end value. May only set time in FIXED
* mode
* @param {Number} [newVal] A time in ms relative to time system epoch.
* @param {TimeConductor.EventTypes} [eventType=TimeConductor.EventTypes.EITHER] The type of event (User, System, or Either)
* @returns {Number} the end date (in milliseconds since some epoch, depending on time system)
*/
TimeConductorLimit.prototype.time = function (newVal, eventType) {
if (arguments.length > 0) {
this.timeVal = newVal;
this.listener.notify(eventType);
}
return this.timeVal;
};
return TimeConductorLimit;
});

View File

@ -1,29 +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 () {
function FixedMode(options) {
}
return FixedMode;
});

View File

@ -1,71 +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 () {
/**
* Class representing the real-time mode of the Time Conductor. In this mode,
* the bounds are updated automatically based on a timing source.
*
* @param options
* @constructor
*/
function RealtimeMode(options) {
this.startDelta = options.startDelta;
this.endDelta = options.endDelta;
this.tickSource = options.tickSource;
this.system = undefined;
}
/**
* @private
*/
this.prototype.initialize = function (conductor) {
var self = this;
/**
* Deltas can be specified for start and end. An end delta will mean
* that the end bound is always in the future by 'endDelta' units
*/
this.startDelta = this.startDelta || conductor.timeSystem().DEFAULT_DELTA;
this.endDelta = this.endDelta || 0;
function setBounds() {
var now = conductor.timeSystem().now();
conductor.bounds({
start: now - self.startDelta,
end: now + self.endDelta
});
}
/**
* If a tick source is specified, listen for ticks
*/
if (this.tickSource) {
this.tickSource.on("tick", setBounds);
}
//Set initial bounds
setBounds();
};
return RealtimeMode;
});

View File

@ -1,74 +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([
"./TimingSource"
], function (TimingSource) {
var ONE_SECOND = 1 * 1000;
/**
* A clock that ticks at the given interval (given in ms).
*
* @implements TimingSource
* @constructor
*/
function LocalClock(interval){
TimingSource.call(this);
this.interval = interval;
this.intervalHandle = undefined;
}
LocalClock.prototype = Object.create(TimingSource.prototype);
/**
* Start the clock ticking. Ticks can be listened to by registering
* listeners of the "tick" event
*/
LocalClock.prototype.attach = function () {
function tick() {
this.emit("tick");
}
this.stop();
this.intervalHandle = setInterval(this.bind(this), this.interval || ONE_SECOND);
};
/**
* Stop the currently running clock. "tick" events will no longer be emitted
*/
LocalClock.prototype.detach = function () {
if (this.intervalHandle) {
clearInterval(this.intervalHandle);
}
};
/**
* @returns {boolean} true if the clock is currently running
*/
LocalClock.prototype.attached = function () {
return !!this.intervalHandle;
}
});

View File

@ -1,56 +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([
"EventEmitter"
], function (EventEmitter) {
/**
* An interface defining a timing source. A timing source is a local or remote source of 'tick' events.
* Could be used to tick when new data is received from a data source.
* @interface
* @constructor
*/
function TimingSource(){
EventEmitter.call(this);
}
TimingSource.prototype = Object.create(EventEmitter.prototype);
/**
* Attach to the timing source. If it's a local clock, will start a local timing loop. If remote, will connect to
* remote source. If event driven (eg. based on data availability) will attach an event listener to telemetry source.
*/
TimingSource.prototype.attach = function () {};
/**
* Detach from the timing source
*/
TimingSource.prototype.detach = function () {};
/**
* @returns {boolean} true if current attached to timing source
*/
TimingSource.prototype.attached = function () {}
});

View File

@ -133,7 +133,7 @@ define([
"telemetry"
],
"delegation": true,
"editable": true
"editable": false
},
{
"name": "Real-time Table",
@ -144,7 +144,7 @@ define([
"telemetry"
],
"delegation": true,
"editable": true
"editable": false
}
],
"directives": [

View File

@ -127,7 +127,16 @@ define([
14400000,
28800000,
43200000,
86400000
86400000,
86400000 * 2,
86400000 * 5,
86400000 * 10,
86400000 * 20,
86400000 * 30,
86400000 * 60,
86400000 * 120,
86400000 * 240,
86400000 * 365
],
"width": 200
}

View File

@ -1,16 +1,22 @@
.l-timeline-gantt {
min-width: 2px;
overflow: hidden;
position: absolute;
top: $timelineSwimlaneGanttVM; bottom: $timelineSwimlaneGanttVM;
.bar {
@include ellipsize();
height: $activityBarH;
line-height: $activityBarH + 2;
line-height: $activityBarH;
padding: 0 $interiorMargin;
span {
display: inline;
$iconW: 20px;
@include absPosDefault();
display: block;
&.s-activity-type {
right: auto; width: $iconW;
text-align: center;
&.timeline {
&:before {
content:"S";
@ -23,7 +29,9 @@
}
}
&.s-title {
text-shadow: rgba(black, 0.1) 0 1px 2px;
overflow: hidden;
text-overflow: ellipsis;
left: $iconW;
}
&.duration {
left: auto;
@ -52,6 +60,10 @@
}
}
}
&.sm .bar span {
// Hide icon and label if width is too small
display: none;
}
}
.edit-mode .s-timeline-gantt,
@ -59,7 +71,7 @@
.handle {
cursor: col-resize;
&.mid {
cursor: move;
cursor: ew-resize;
}
}
}

View File

@ -32,20 +32,10 @@
}
.s-timeline-gantt {
$br: $controlCr;
.bar {
color: $colorGanttBarFg;
@include activityBg($colorGanttBarBg);
border-radius: $br;
box-shadow: $shdwGanttBar;
&.expanded {
@include border-top-radius($br);
@include border-bottom-radius(0);
}
&.leaf {
@include border-top-radius(0);
@include border-bottom-radius($br);
}
.s-toggle {
color: $colorGanttToggle;
}

View File

@ -52,6 +52,9 @@
// Tree area with item title
right: auto; // Set this to auto and uncomment width below when additional tabular columns are added
width: $timelineTabularTitleW;
.l-swimlanes-holder {
bottom: $scrollbarTrackSize;
}
}
&.l-tabular-r {
// Start, end, duration, activity modes columns
@ -67,6 +70,7 @@
&.l-timeline-gantt {
.l-swimlanes-holder {
@include scrollV(scroll);
bottom: $scrollbarTrackSize;
}
}
&.l-timeline-resource-legend {

View File

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

View File

@ -103,6 +103,13 @@
<!-- TOP PANE GANTT BARS -->
<div class="split-pane-component l-timeline-pane t-pane-h l-pane-top t-timeline-gantt l-timeline-gantt s-timeline-gantt">
<div class="l-hover-btns-holder s-hover-btns-holder t-btns-zoom">
<a class="t-btn l-btn s-btn"
ng-click="zoomController.fit()"
ng-show="true"
title="Zoom to fit">
<span class="ui-symbol icon zoom-in">I</span>
</a>
<a class="t-btn l-btn s-btn"
ng-click="zoomController.zoom(-1)"
ng-show="true"
@ -121,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.toPixels(zoomController.duration()),
fullWidth: timelineController.width(zoomController),
start: scroll.x,
width: scroll.width,
step: zoomController.toPixels(zoomController.zoom()),

View File

@ -97,6 +97,8 @@ define(
}
}
$scope.$watch("configuration", swimlanePopulator.configure);
// Recalculate swimlane state on changes
$scope.$watch("domainObject", swimlanePopulator.populate);

View File

@ -32,16 +32,26 @@ define(
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 sz = zoomLevels[zoomLevels.length - 1];
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];
}
function toPixels(millis) {
return tickWidth * millis / zoomLevels[zoomIndex];
}
// Get/set zoom level
function setZoomLevel(level) {
if (!isNaN(level)) {
@ -53,20 +63,27 @@ define(
}
}
// Persist current zoom level
function storeZoom() {
var isEditMode = $scope.commit &&
$scope.domainObject &&
$scope.domainObject.hasCapability('editor') &&
$scope.domainObject.getCapability('editor').inEditContext();
if (isEditMode) {
$scope.configuration = $scope.configuration || {};
$scope.configuration.zoomLevel = zoomIndex;
$scope.commit();
function initializeZoomFromTimespan(timespan) {
var timelineDuration = timespan.getDuration();
zoomIndex = 0;
while (toMillis(bounds.width) < timelineDuration &&
zoomIndex < zoomLevels.length - 1) {
zoomIndex += 1;
}
bounds.x = toPixels(timespan.getStart());
}
function initializeZoom() {
if ($scope.domainObject) {
$scope.domainObject.useCapability('timespan')
.then(initializeZoomFromTimespan);
}
}
$scope.$watch("configuration.zoomLevel", setZoomLevel);
$scope.$watch("scroll", function (scroll) {
bounds = scroll;
});
$scope.$watch("domainObject", initializeZoom);
return {
/**
@ -83,27 +100,29 @@ define(
zoom: function (amount) {
// Update the zoom level if called with an argument
if (arguments.length > 0 && !isNaN(amount)) {
var center = this.toMillis(bounds.x + bounds.width / 2);
setZoomLevel(zoomIndex + amount);
storeZoom(zoomIndex);
bounds.x = this.toPixels(center) - bounds.width / 2;
}
return zoomLevels[zoomIndex];
},
/**
* Set the zoom level to fit the bounds of the timeline
* being viewed.
*/
fit: initializeZoom,
/**
* Get the width, in pixels, of a specific time duration at
* the current zoom level.
* @returns {number} the number of pixels
*/
toPixels: function (millis) {
return tickWidth * millis / zoomLevels[zoomIndex];
},
toPixels: toPixels,
/**
* Get the time duration, in milliseconds, occupied by the
* width (specified in pixels) at the current zoom level.
* @returns {number} the number of pixels
*/
toMillis: function (pixels) {
return (pixels / tickWidth) * zoomLevels[zoomIndex];
},
toMillis: toMillis,
/**
* Get or set the current displayed duration. If used as a
* setter, this will typically be rounded up to ensure extra

View File

@ -43,8 +43,7 @@ define(
var swimlanes = [],
start = Number.POSITIVE_INFINITY,
end = Number.NEGATIVE_INFINITY,
colors = (configuration.colors || {}),
assigner = new TimelineColorAssigner(colors),
assigner,
lastDomainObject;
// Track extremes of start/end times
@ -152,8 +151,15 @@ define(
recalculateSwimlanes(lastDomainObject);
}
function initialize() {
var colors = (configuration.colors || {});
assigner = new TimelineColorAssigner(colors);
configuration.colors = colors;
recalculateSwimlanes(lastDomainObject);
}
// Ensure colors are exposed in configuration
configuration.colors = colors;
initialize();
return {
/**
@ -188,6 +194,15 @@ define(
*/
end: function () {
return end;
},
/**
* Pass a new configuration object (to retrieve and store
* swimlane configuration)
* @param newConfig
*/
configure: function (newConfig) {
configuration = newConfig;
initialize();
}
};
}

View File

@ -68,6 +68,14 @@ define(
};
}
function fireWatch(expr, value) {
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
});
}
beforeEach(function () {
var mockA, mockB, mockUtilization, mockPromise, mockGraph, testCapabilities;
@ -141,9 +149,15 @@ define(
expect(mockScope.scroll.y).toEqual(0);
});
it("watches for a configuration object", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"configuration",
jasmine.any(Function)
);
});
it("repopulates when modifications are made", function () {
var fnWatchCall,
strWatchCall;
var fnWatchCall;
// Find the $watch that was given a function
mockScope.$watch.calls.forEach(function (call) {
@ -151,16 +165,11 @@ define(
// white-box: we know the first call is
// the one we're looking for
fnWatchCall = fnWatchCall || call;
} else if (typeof call.args[0] === 'string') {
strWatchCall = strWatchCall || call;
}
});
// Make sure string watch was for domainObject
expect(strWatchCall.args[0]).toEqual('domainObject');
// Initially populate
strWatchCall.args[1](mockDomainObject);
fireWatch('domainObject', mockDomainObject);
// There should be to swimlanes
expect(controller.swimlanes().length).toEqual(2);
@ -182,23 +191,23 @@ define(
// order of $watch calls in TimelineController.
// Initially populate
mockScope.$watch.calls[0].args[1](mockDomainObject);
fireWatch('domainObject', mockDomainObject);
// Verify precondition - no graphs
expect(controller.graphs().length).toEqual(0);
// Execute the watch function for graph state
tmp = mockScope.$watch.calls[2].args[0]();
tmp = mockScope.$watch.calls[3].args[0]();
// Change graph state
testConfiguration.graph = { a: true, b: true };
// Verify that this would have triggered a watch
expect(mockScope.$watch.calls[2].args[0]())
expect(mockScope.$watch.calls[3].args[0]())
.not.toEqual(tmp);
// Run the function the watch would have triggered
mockScope.$watch.calls[2].args[1]();
mockScope.$watch.calls[3].args[1]();
// Should have some graphs now
expect(controller.graphs().length).toEqual(2);
@ -211,7 +220,7 @@ define(
mockZoom.duration.andReturn(12345);
// Initially populate
mockScope.$watch.calls[0].args[1](mockDomainObject);
fireWatch('domainObject', mockDomainObject);
expect(controller.width(mockZoom)).toEqual(54321);
// Verify interactions; we took zoom's duration for our start/end,

View File

@ -32,11 +32,7 @@ define(
beforeEach(function () {
testConfiguration = {
levels: [
1000,
2000,
3500
],
levels: [1000, 2000, 3500],
width: 12321
};
mockScope = jasmine.createSpyObj("$scope", ['$watch']);
@ -74,32 +70,61 @@ define(
expect(controller.zoom()).toEqual(3500);
});
it("does not normally persist zoom changes", function () {
controller.zoom(1);
expect(mockScope.commit).not.toHaveBeenCalled();
it("observes scroll bounds", function () {
expect(mockScope.$watch)
.toHaveBeenCalledWith("scroll", jasmine.any(Function));
});
it("persists zoom changes in Edit mode", function () {
mockScope.domainObject = jasmine.createSpyObj(
'domainObject',
['hasCapability', 'getCapability']
);
mockScope.domainObject.hasCapability.andCallFake(function (c) {
return c === 'editor';
describe("when watches have fired", function () {
var mockDomainObject,
mockPromise,
mockTimespan,
testStart,
testEnd;
beforeEach(function () {
testStart = 3000;
testEnd = 5500;
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getId',
'getModel',
'getCapability',
'useCapability'
]);
mockPromise = jasmine.createSpyObj('promise', ['then']);
mockTimespan = jasmine.createSpyObj('timespan', [
'getStart',
'getEnd',
'getDuration'
]);
mockDomainObject.useCapability.andCallFake(function (c) {
return c === 'timespan' && mockPromise;
});
mockPromise.then.andCallFake(function (callback) {
callback(mockTimespan);
});
mockTimespan.getStart.andReturn(testStart);
mockTimespan.getEnd.andReturn(testEnd);
mockTimespan.getDuration.andReturn(testEnd - testStart);
mockScope.scroll = { x: 0, width: 20000 };
mockScope.domainObject = mockDomainObject;
mockScope.$watch.calls.forEach(function (call) {
call.args[1](mockScope[call.args[0]]);
});
});
mockScope.domainObject.getCapability.andCallFake(function (c) {
if (c === 'editor') {
return {
inEditContext: function () {
return true;
}
};
}
it("zooms to fit the timeline", function () {
var x1 = mockScope.scroll.x,
x2 = mockScope.scroll.x + mockScope.scroll.width;
expect(Math.round(controller.toMillis(x1)))
.toEqual(testStart);
expect(Math.round(controller.toMillis(x2)))
.toBeGreaterThan(testEnd);
});
controller.zoom(1);
expect(mockScope.commit).toHaveBeenCalled();
expect(mockScope.configuration.zoomLevel)
.toEqual(jasmine.any(Number));
});
});

View File

@ -177,6 +177,10 @@ define(
// representation to store local variables into.
$scope.representation = {};
// Change templates (passing in undefined to clear
// if we don't have enough info to show a template.)
changeTemplate(canRepresent ? representation : undefined);
// Any existing representers are no longer valid; release them.
destroyRepresenters();
@ -222,10 +226,6 @@ define(
// next change object/key pair changes
toClear = uses.concat(['model']);
}
// Change templates (passing in undefined to clear
// if we don't have enough info to show a template.)
changeTemplate(canRepresent ? representation : undefined);
}
// Update the representation when the key changes (e.g. if a

View File

@ -46,7 +46,7 @@ define(
// Find the relevant scope...
var rect,
scope = element.scope && element.scope();
if (scope && scope.$broadcast) {
// Get the representation's bounds, to convert
// drop position

View File

@ -194,21 +194,6 @@ define(
.toHaveBeenCalledWith(testViews[1]);
});
it("exposes configuration before changing templates", function () {
var observedConfiguration;
mockChangeTemplate.andCallFake(function () {
observedConfiguration = mockScope.configuration;
});
mockScope.key = "xyz";
mockScope.domainObject = mockDomainObject;
fireWatch('key', mockScope.key);
fireWatch('domainObject', mockDomainObject);
expect(observedConfiguration).toBeDefined();
});
it("does not load templates until there is an object", function () {
mockScope.key = "xyz";

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.

View File

@ -19,32 +19,31 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
"./TimingSource"
], function (TimingSource) {
/**
* A timing source that 'ticks' when new data is available
* @implements TimingSource
* @constructor
*/
function DataAvailabilityTicker(){
TimingSource.call(this);
}
DataAvailabilityTicker.prototype = Object.create(TimingSource.prototype);
/**
* Registers an event listener to listen for data availability at telemetry source
*/
DataAvailabilityTicker.prototype.attach = function () {};
/**
* Unregisters event listeners, seasing tick events.
*/
DataAvailabilityTicker.prototype.detach = function () {};
DataAvailabilityTicker.prototype.attached = function () {}
'./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;
};
});

View File

@ -0,0 +1,90 @@
define([
'legacyRegistry',
'./src/ExampleTelemetryServerAdapter',
'./src/ExampleTelemetryInitializer',
'./src/ExampleTelemetryModelProvider'
], function (
legacyRegistry,
ExampleTelemetryServerAdapter,
ExampleTelemetryInitializer,
ExampleTelemetryModelProvider
) {
legacyRegistry.register("tutorials/telemetry", {
"name": "Example Telemetry Adapter",
"extensions": {
"types": [
{
"name": "Spacecraft",
"key": "example.spacecraft",
"glyph": "o"
},
{
"name": "Subsystem",
"key": "example.subsystem",
"glyph": "o",
"model": { "composition": [] }
},
{
"name": "Measurement",
"key": "example.measurement",
"glyph": "T",
"model": { "telemetry": {} },
"telemetry": {
"source": "example.source",
"domains": [
{
"name": "Time",
"key": "timestamp"
}
]
}
}
],
"roots": [
{
"id": "example:sc",
"priority": "preferred",
"model": {
"type": "example.spacecraft",
"name": "My Spacecraft",
"composition": []
}
}
],
"services": [
{
"key": "example.adapter",
"implementation": "ExampleTelemetryServerAdapter.js",
"depends": [ "$q", "EXAMPLE_WS_URL" ]
}
],
"constants": [
{
"key": "EXAMPLE_WS_URL",
"priority": "fallback",
"value": "ws://localhost:8081"
}
],
"runs": [
{
"implementation": "ExampleTelemetryInitializer.js",
"depends": [ "example.adapter", "objectService" ]
}
],
"components": [
{
"provides": "modelService",
"type": "provider",
"implementation": "ExampleTelemetryModelProvider.js",
"depends": [ "example.adapter", "$q" ]
},
{
"provides": "telemetryService",
"type": "provider",
"implementation": "ExampleTelemetryProvider.js",
"depends": [ "example.adapter", "$q" ]
}
]
}
});
});

View File

@ -0,0 +1,47 @@
define(
function () {
"use strict";
var TAXONOMY_ID = "example:sc",
PREFIX = "example_tlm:";
function ExampleTelemetryInitializer(adapter, objectService) {
// Generate a domain object identifier for a dictionary element
function makeId(element) {
return PREFIX + element.identifier;
}
// When the dictionary is available, add all subsystems
// to the composition of My Spacecraft
function initializeTaxonomy(dictionary) {
// Get the top-level container for dictionary objects
// from a group of domain objects.
function getTaxonomyObject(domainObjects) {
return domainObjects[TAXONOMY_ID];
}
// Populate
function populateModel(taxonomyObject) {
return taxonomyObject.useCapability(
"mutation",
function (model) {
model.name =
dictionary.name;
model.composition =
dictionary.subsystems.map(makeId);
}
);
}
// Look up My Spacecraft, and populate it accordingly.
objectService.getObjects([TAXONOMY_ID])
.then(getTaxonomyObject)
.then(populateModel);
}
adapter.dictionary().then(initializeTaxonomy);
}
return ExampleTelemetryInitializer;
}
);

View File

@ -0,0 +1,78 @@
define(
function () {
"use strict";
var PREFIX = "example_tlm:",
FORMAT_MAPPINGS = {
float: "number",
integer: "number",
string: "string"
};
function ExampleTelemetryModelProvider(adapter, $q) {
var modelPromise, empty = $q.when({});
// Check if this model is in our dictionary (by prefix)
function isRelevant(id) {
return id.indexOf(PREFIX) === 0;
}
// Build a domain object identifier by adding a prefix
function makeId(element) {
return PREFIX + element.identifier;
}
// Create domain object models from this dictionary
function buildTaxonomy(dictionary) {
var models = {};
// Create & store a domain object model for a measurement
function addMeasurement(measurement) {
var format = FORMAT_MAPPINGS[measurement.type];
models[makeId(measurement)] = {
type: "example.measurement",
name: measurement.name,
telemetry: {
key: measurement.identifier,
ranges: [{
key: "value",
name: "Value",
units: measurement.units,
format: format
}]
}
};
}
// Create & store a domain object model for a subsystem
function addSubsystem(subsystem) {
var measurements =
(subsystem.measurements || []);
models[makeId(subsystem)] = {
type: "example.subsystem",
name: subsystem.name,
composition: measurements.map(makeId)
};
measurements.forEach(addMeasurement);
}
(dictionary.subsystems || []).forEach(addSubsystem);
return models;
}
// Begin generating models once the dictionary is available
modelPromise = adapter.dictionary().then(buildTaxonomy);
return {
getModels: function (ids) {
// Return models for the dictionary only when they
// are relevant to the request.
return ids.some(isRelevant) ? modelPromise : empty;
}
};
}
return ExampleTelemetryModelProvider;
}
);

View File

@ -0,0 +1,80 @@
define(
['./ExampleTelemetrySeries'],
function (ExampleTelemetrySeries) {
"use strict";
var SOURCE = "example.source";
function ExampleTelemetryProvider(adapter, $q) {
var subscribers = {};
// Used to filter out requests for telemetry
// from some other source
function matchesSource(request) {
return (request.source === SOURCE);
}
// Listen for data, notify subscribers
adapter.listen(function (message) {
var packaged = {};
packaged[SOURCE] = {};
packaged[SOURCE][message.id] =
new ExampleTelemetrySeries([message.value]);
(subscribers[message.id] || []).forEach(function (cb) {
cb(packaged);
});
});
return {
requestTelemetry: function (requests) {
var packaged = {},
relevantReqs = requests.filter(matchesSource);
// Package historical telemetry that has been received
function addToPackage(history) {
packaged[SOURCE][history.id] =
new ExampleTelemetrySeries(history.value);
}
// Retrieve telemetry for a specific measurement
function handleRequest(request) {
var key = request.key;
return adapter.history(key).then(addToPackage);
}
packaged[SOURCE] = {};
return $q.all(relevantReqs.map(handleRequest))
.then(function () { return packaged; });
},
subscribe: function (callback, requests) {
var keys = requests.filter(matchesSource)
.map(function (req) { return req.key; });
function notCallback(cb) {
return cb !== callback;
}
function unsubscribe(key) {
subscribers[key] =
(subscribers[key] || []).filter(notCallback);
if (subscribers[key].length < 1) {
adapter.unsubscribe(key);
}
}
keys.forEach(function (key) {
subscribers[key] = subscribers[key] || [];
adapter.subscribe(key);
subscribers[key].push(callback);
});
return function () {
keys.forEach(unsubscribe);
};
}
};
}
return ExampleTelemetryProvider;
}
);

View File

@ -0,0 +1,23 @@
/*global define*/
define(
function () {
"use strict";
function ExampleTelemetrySeries(data) {
return {
getPointCount: function () {
return data.length;
},
getDomainValue: function (index) {
return (data[index] || {}).timestamp;
},
getRangeValue: function (index) {
return (data[index] || {}).value;
}
};
}
return ExampleTelemetrySeries;
}
);

View File

@ -0,0 +1,60 @@
define(
[],
function () {
"use strict";
function ExampleTelemetryServerAdapter($q, wsUrl) {
var ws = new WebSocket(wsUrl),
histories = {},
listeners = [],
dictionary = $q.defer();
// Handle an incoming message from the server
ws.onmessage = function (event) {
var message = JSON.parse(event.data);
switch (message.type) {
case "dictionary":
dictionary.resolve(message.value);
break;
case "history":
histories[message.id].resolve(message);
delete histories[message.id];
break;
case "data":
listeners.forEach(function (listener) {
listener(message);
});
break;
}
};
// Request dictionary once connection is established
ws.onopen = function () {
ws.send("dictionary");
};
return {
dictionary: function () {
return dictionary.promise;
},
history: function (id) {
histories[id] = histories[id] || $q.defer();
ws.send("history " + id);
return histories[id].promise;
},
subscribe: function (id) {
ws.send("subscribe " + id);
},
unsubscribe: function (id) {
ws.send("unsubscribe " + id);
},
listen: function (callback) {
listeners.push(callback);
}
};
}
return ExampleTelemetryServerAdapter;
}
);

59
tutorials/todo/bundle.js Normal file
View File

@ -0,0 +1,59 @@
define([
'legacyRegistry',
'./src/controllers/TodoController'
], function (
legacyRegistry,
TodoController
) {
legacyRegistry.register("tutorials/todo", {
"name": "To-do Plugin",
"description": "Allows creating and editing to-do lists.",
"extensions": {
"views": [
{
"key": "example.todo",
"type": "example.todo",
"glyph": "2",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true,
"toolbar": {
"sections": [
{
"items": [
{
"text": "Add Task",
"glyph": "+",
"method": "addTask",
"control": "button"
}
]
},
{
"items": [
{
"glyph": "Z",
"method": "removeTask",
"control": "button"
}
]
}
]
}
}
],
"controllers": [
{
"key": "TodoController",
"implementation": TodoController,
"depends": [ "$scope", "dialogService" ]
}
],
"stylesheets": [
{
"stylesheetUrl": "css/todo.css"
}
]
}
});
});

View File

@ -0,0 +1,29 @@
<div ng-controller="TodoController" class="example-todo">
<div class="example-button-group">
<a ng-class="{ selected: checkVisibility(true) }"
ng-click="setVisibility(true)">All</a>
<a ng-class="{ selected: checkVisibility(false, false) }"
ng-click="setVisibility(false, false)">Incomplete</a>
<a ng-class="{ selected: checkVisibility(false, true) }"
ng-click="setVisibility(false, true)">Complete</a>
</div>
<ul>
<li ng-repeat="task in model.tasks"
ng-class="{ 'example-task-completed': task.completed }"
ng-if="showTask(task)">
<input type="checkbox"
ng-checked="task.completed"
ng-click="toggleCompletion($index)">
<span ng-click="selectTask($index)"
ng-class="{ selected: isSelected($index) }"
class="example-task-description">
{{task.description}}
</span>
</li>
</ul>
<div ng-if="model.tasks.length < 1" class="example-message">
There are no tasks to show.
</div>
</div>

View File

@ -0,0 +1,97 @@
define(function () {
// Form to display when adding new tasks
var NEW_TASK_FORM = {
name: "Add a Task",
sections: [{
rows: [{
name: 'Description',
key: 'description',
control: 'textfield',
required: true
}]
}]
};
function TodoController($scope, dialogService) {
var showAll = true,
showCompleted;
// Persist changes made to a domain object's model
function persist() {
var persistence =
$scope.domainObject.getCapability('persistence');
return persistence && persistence.persist();
}
// Remove a task
function removeTaskAtIndex(taskIndex) {
$scope.domainObject.useCapability('mutation', function (model) {
model.tasks.splice(taskIndex, 1);
});
persist();
}
// Add a task
function addNewTask(task) {
$scope.domainObject.useCapability('mutation', function (model) {
model.tasks.push(task);
});
persist();
}
// Change which tasks are visible
$scope.setVisibility = function (all, completed) {
showAll = all;
showCompleted = completed;
};
// Check if current visibility settings match
$scope.checkVisibility = function (all, completed) {
return showAll ? all : (completed === showCompleted);
};
// Toggle the completion state of a task
$scope.toggleCompletion = function (taskIndex) {
$scope.domainObject.useCapability('mutation', function (model) {
var task = model.tasks[taskIndex];
task.completed = !task.completed;
});
persist();
};
// Check whether a task should be visible
$scope.showTask = function (task) {
return showAll || (showCompleted === !!(task.completed));
};
// Handle selection state in edit mode
if ($scope.selection) {
// Expose the ability to select tasks
$scope.selectTask = function (taskIndex) {
$scope.selection.select({
removeTask: function () {
removeTaskAtIndex(taskIndex);
$scope.selection.deselect();
},
taskIndex: taskIndex
});
};
// Expose a check for current selection state
$scope.isSelected = function (taskIndex) {
return ($scope.selection.get() || {}).taskIndex ===
taskIndex;
};
// Expose a view-level selection proxy
$scope.selection.proxy({
addTask: function () {
dialogService.getUserInput(NEW_TASK_FORM, {})
.then(addNewTask);
}
});
}
}
return TodoController;
});

19
tutorials/todo/todo.js Normal file
View File

@ -0,0 +1,19 @@
define(function () {
return function todoPlugin(mct) {
var todoType = new mct.Type({
metadata: {
label: "To-Do List",
glyph: "2",
description: "A list of things that need to be done."
},
initialize: function (model) {
model.tasks = [];
},
creatable: true
});
mct.type('example.todo', todoType);
return mct;
};
});