Merge remote-tracking branch 'github/master' into open450b

Merge latest from master branch to reconcile conflicts
for https://github.com/nasa/openmctweb/pull/469

Conflicts:
	platform/commonUI/general/bundle.json
This commit is contained in:
Victor Woeltjen 2016-01-08 15:36:10 -08:00
commit 0036974c60
28 changed files with 2939 additions and 413 deletions

3
docs/src/design/index.md Normal file
View File

@ -0,0 +1,3 @@
Design proposals:
* [API Redesign](proposals/APIRedesign.md)

View File

@ -0,0 +1,338 @@
# API Refactoring
This document summarizes a path toward implementing API changes
from the [API Redesign](../proposals/APIRedesign.md) for Open MCT Web
v1.0.0.
# Goals
These plans are intended to minimize:
* Waste; avoid allocating effort to temporary changes.
* Downtime; avoid making changes in large increments that blocks
delivery of new features for substantial periods of time.
* Risk; ensure that changes can be validated quickly, avoid putting
large effort into changes that have not been validated.
# Plan
```nomnoml
#comment: This diagram is in nomnoml syntax and should be rendered.
#comment: See https://github.com/nasa/openmctweb/issues/264#issuecomment-167166471
[<start> Start]->[<state> Imperative bundle registration]
[<state> Imperative bundle registration]->[<state> Build and packaging]
[<state> Imperative bundle registration]->[<state> Refactor API]
[<state> Build and packaging |
[<start> Start]->[<state> Incorporate a build step]
[<state> Incorporate a build step |
[<start> Start]->[<state> Choose package manager]
[<start> Start]->[<state> Choose build system]
[<state> Choose build system]<->[<state> Choose package manager]
[<state> Choose package manager]->[<state> Implement]
[<state> Choose build system]->[<state> Implement]
[<state> Implement]->[<end> End]
]->[<state> Separate repositories]
[<state> Separate repositories]->[<end> End]
]->[<state> Release candidacy]
[<start> Start]->[<state> Design registration API]
[<state> Design registration API |
[<start> Start]->[<state> Decide on role of Angular]
[<state> Decide on role of Angular]->[<state> Design API]
[<state> Design API]->[<choice> Passes review?]
[<choice> Passes review?] no ->[<state> Design API]
[<choice> Passes review?]-> yes [<end> End]
]->[<state> Refactor API]
[<state> Refactor API |
[<start> Start]->[<state> Imperative extension registration]
[<state> Imperative extension registration]->[<state> Refactor individual extensions]
[<state> Refactor individual extensions |
[<start> Start]->[<state> Prioritize]
[<state> Prioritize]->[<choice> Sufficient value added?]
[<choice> Sufficient value added?] no ->[<end> End]
[<choice> Sufficient value added?] yes ->[<state> Design]
[<state> Design]->[<choice> Passes review?]
[<choice> Passes review?] no ->[<state> Design]
[<choice> Passes review?]-> yes [<state> Implement]
[<state> Implement]->[<end> End]
]->[<state> Remove legacy bundle support]
[<state> Remove legacy bundle support]->[<end> End]
]->[<state> Release candidacy]
[<state> Release candidacy |
[<start> Start]->[<state> Verify |
[<start> Start]->[<choice> API well-documented?]
[<start> Start]->[<choice> API well-tested?]
[<choice> API well-documented?]-> no [<state> Write documentation]
[<choice> API well-documented?] yes ->[<end> End]
[<state> Write documentation]->[<choice> API well-documented?]
[<choice> API well-tested?]-> no [<state> Write test cases]
[<choice> API well-tested?]-> yes [<end> End]
[<state> Write test cases]->[<choice> API well-tested?]
]
[<start> Start]->[<state> Validate |
[<start> Start]->[<choice> Passes review?]
[<start> Start]->[<state> Use internally]
[<state> Use internally]->[<choice> Proves useful?]
[<choice> Passes review?]-> no [<state> Address feedback]
[<state> Address feedback]->[<choice> Passes review?]
[<choice> Passes review?] yes -> [<end> End]
[<choice> Proves useful?] yes -> [<end> End]
[<choice> Proves useful?] no -> [<state> Fix problems]
[<state> Fix problems]->[<state> Use internally]
]
[<state> Validate]->[<end> End]
[<state> Verify]->[<end> End]
]->[<state> Release]
[<state> Release]->[<end> End]
```
## Step 1. Imperative bundle registration
Register whole bundles imperatively, using their current format.
For example, in each bundle add a `bundle.js` file:
```js
define([
'mctRegistry',
'json!bundle.json'
], function (mctRegistry, bundle) {
mctRegistry.install(bundle, "path/to/bundle");
});
```
Where `mctRegistry.install` is placeholder API that wires into the
existing bundle registration mechanisms. The main point of entry
would need to be adapted to clearly depend on these bundles
(in the require sense of a dependency), and the framework layer
would need to implement and integrate with this transitional
API.
Benefits:
* Achieves an API Redesign goal with minimal immediate effort.
* Conversion to an imperative syntax may be trivially automated.
* Minimal change; reuse existing bundle definitions, primarily.
* Allows early validation of switch to imperative; unforeseen
consequences of the change may be detected at this point.
* Allows implementation effort to progress in parallel with decisions
about API changes, including fundamental ones such as the role of
Angular. May act in some sense as a prototype to inform those
decisions.
* Creates a location (framework layer) where subsequent changes to
the manner in which extensions are registered may be centralized.
When there is a one-to-one correspondence between the existing
form of an extension and its post-refactor form, adapters can be
written here to defer the task of making changes ubiquitously
throughout bundles, allowing for earlier validation and
verification of those changes, and avoiding ubiquitous changes
which might require us to go dark. (Mitigates
["greenfield paradox"](http://stepaheadsoftware.blogspot.com/2012/09/greenfield-or-refactor-legacy-code-base.html);
want to add value with new API but don't want to discard value
of tested/proven legacy codebase.)
Detriments:
* Requires transitional API to be implemented/supported; this is
waste. May mitigate this by time-bounding the effort put into
this step to ensure that waste is minimal.
Note that API changes at this point do not meaningfully reflect
the desired 1.0.0 API, so no API reviews are necessary.
## Step 2. Incorporate a build step
After the previous step is completed, there should be a
straightforward dependency graph among AMD modules, and an
imperative (albeit transitional) API allowing for other plugins
to register themselves. This should allow for a build step to
be included in a straightforward fashion.
Some goals for this build step:
* Compile (and, preferably, optimize/minify) Open MCT Web
sources into a single `.js` file.
* It is desirable to do the same for HTML sources, but
may wish to defer this until a subsequent refactoring
step if appropriate.
* Provide non-code assets in a format that can be reused by
derivative projects in a straightforward fashion.
Should also consider which dependency/packaging manager should
be used by dependent projects to obtain Open MCT Web. Approaches
include:
1. Plain `npm`. Dependents then declare their dependency with
`npm` and utilize built sources and assets in a documented
fashion. (Note that there are
[documented challenges](http://blog.npmjs.org/post/101775448305/npm-and-front-end-packaging)
in using `npm` in this fashion.)
2. Build with `npm`, but recommend dependents install using
`bower`, as this is intended for front-end development. This may
require checking in built products, however, which
we wish to avoid (this could be solved by maintaining
a separate repository for built products.)
In all cases, there is a related question of which build system
to use for asset generation/management and compilation/minification/etc.
1. [`webpack`](https://webpack.github.io/)
is well-suited in principle, as it is specifically
designed for modules with non-JS dependencies. However,
there may be limitations and/or undesired behavior here
(for instance, CSS dependencies get in-lined as style tags,
removing our ability to control ordering) so it may
2. `gulp` or `grunt`. Commonplace, but both still require
non-trivial coding and/or configuration in order to produce
appropriate build artifacts.
3. [Just `npm`](http://blog.keithcirkel.co.uk/how-to-use-npm-as-a-build-tool/).
Reduces the amount of tooling being used, but may introduce
some complexity (e.g. custom scripts) to the build process,
and may reduce portability.
## Step 3. Separate repositories
Refactor existing applications built on Open MCT Web such that they
are no longer forks, but instead separate projects with a dependency
on the built artifacts from Step 2.
Note that this is achievable already using `bower` (see `warp-bower`
branch at http://developer.nasa.gov/mct/warp for an example.)
However, changes involved in switching to an imperative API and
introducing a build process may change (and should simplify) the
approach used to utilize Open MCT Web as a dependency, so these
changes should be introduced first.
## Step 4. Design registration API
Design the registration API that will replace declarative extension
categories and extensions (including Angular built-ins and composite
services.)
This may occur in parallel with implementation steps.
It will be necessary
to have a decision about the role of Angular at this point; are extensions
registered via provider configuration (Angular), or directly in some
exposed registry?
Success criteria here should be based on peer review. Scope of peer
review should be based on perceived risk/uncertainty surrounding
proposed changes, to avoid waste; may wish to limit this review to
the internal team. (The extent to which external
feedback is available is limited, but there is an inherent timeliness
to external review; need to balance this.)
Benefits:
* Solves the "general case" early, allowing for early validation.
Note that in specific cases, it may be desirable to refactor some
current "extension category" in a manner that will not appear as
registries, _or_ to locate these in different
namespaces, _or_ to remove/replace certain categories entirely.
This work is deferred intentionally to allow for a solution of the
general case.
## Step 5. Imperative extension registration
Register individual extensions imperatively, implementing API changes
from the previous step. At this stage, _usage_ of the API may be confined
to a transitional adapter in the framework layer; bundles may continue
to utilize the transitional API for registering extensions in the
legacy format.
An important, ongoing sub-task here will be to discover and define dependencies
among bundles. Composite services and extension categories are presently
"implicit"; after the API redesign, these will become "explicit", insofar
as some specific component will be responsible for creating any registries.
As such, "bundles" which _use_ specific registries will need to have an
enforceable dependency (e.g. require) upon those "bundles" which
_declare_ those registries.
## Step 6. Refactor individual extensions
Refactor individual extension categories and/or services that have
been identified as needing changes. This includes, but is not
necessarily limited to:
* Views/Representations/Templates (refactored into "components.")
* Capabilities (refactored into "roles", potentially.)
* Telemetry (from `TelemetrySeries` to `TelemetryService`.)
Changes should be made one category at a time (either serially
or separately in parallel) and should involve a tight cycle of:
1. Prioritization/reprioritization; highest-value API improvements
should be done first.
2. Design.
3. Review. Refactoring individual extensions will require significant
effort (likely the most significant effort in the process) so changes
should be validated early to minimize risk/waste.
4. Implementation. These changes will not have a one-to-one relationship
with existing extensions, so changes cannot be centralized; usages
will need to be updated across all "bundles" instead of centralized
in a legacy adapter. If changes are of sufficient complexity, some
planning should be done to spread out the changes incrementally.
By necessity, these changes may break functionality in applications
built using Open MCT Web. On a case-by-case basis, should consider
providing temporary "legacy support" to allow downstream updates
to occur as a separate task; the relevant trade here is between
waste/effort required to maintain legacy support, versus the
downtime which may be introduced by making these changes simultaneously
across several repositories.
## Step 7. Remove legacy bundle support
Update bundles to remove any usages of legacy support for bundles
(including that used by dependent projects.) Then, remove legacy
support from Open MCT Web.
## Step 8. Release candidacy
Once API changes are complete, Open MCT Web should enter a release
candidacy cycle. Important things to look at here:
* Are changes really complete?
* Are they sufficiently documented?
* Are they sufficiently tested?
* Are changes really sufficient?
* Do reviewers think they are usable?
* Does the development team find them useful in practice? This
will require calendar time to ascertain; should allocate time
for this, particularly in alignment with the sprint/release
cycle.
* Has learning curve been measurably decreased? Comparing a to-do
list tutorial to [other examples(http://todomvc.com/) could
provide an empirical basis to this. How much code is required?
How much explanation is required? How many dependencies must
be installed before initial setup?
* Does the API offer sufficient power to implement the extensions we
anticipate?
* Any open API-related issues which should block a 1.0.0 release?
Any problems identified during release candidacy will require
subsequent design changes and planning.
## Step 9. Release
Once API changes have been verified and validated, proceed
with release, including:
* Tagging as version 1.0.0 (at an appropriate time in the
sprint/release cycle.)
* Close any open issues which have been resolved (or made obsolete)
by API changes.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,251 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Reducing interface depth (the bundle.json version)](#reducing-interface-depth-the-bundlejson-version)
- [Imperitive component registries](#imperitive-component-registries)
- [Get rid of "extension category" concept.](#get-rid-of-extension-category-concept)
- [Reduce number and depth of extension points](#reduce-number-and-depth-of-extension-points)
- [Composite services should not be the default](#composite-services-should-not-be-the-default)
- [Get rid of views, representations, and templates.](#get-rid-of-views-representations-and-templates)
- [Reducing interface depth (The angular discussion)](#reducing-interface-depth-the-angular-discussion)
- [More angular: for all services](#more-angular-for-all-services)
- [Less angular: only for views](#less-angular-only-for-views)
- [Standard packaging and build system](#standard-packaging-and-build-system)
- [Use systemjs for module loading](#use-systemjs-for-module-loading)
- [Use gulp or grunt for standard tooling](#use-gulp-or-grunt-for-standard-tooling)
- [Package openmctweb as single versioned file.](#package-openmctweb-as-single-versioned-file)
- [Misc Improvements](#misc-improvements)
- [Refresh on navigation](#refresh-on-navigation)
- [Move persistence adapter to promise rejection.](#move-persistence-adapter-to-promise-rejection)
- [Remove bulk requests from providers](#remove-bulk-requests-from-providers)
- [Notes on current API proposals:](#notes-on-current-api-proposals)
- [[1] Footnote: The angular debacle](#1-footnote-the-angular-debacle)
- ["Do or do not, there is no try"](#do-or-do-not-there-is-no-try)
- [A lack of commitment](#a-lack-of-commitment)
- [Commitment is good!](#commitment-is-good)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Reducing interface depth (the bundle.json version)
## Imperitive component registries
Transition component registries to javascript, get rid of bundle.json and bundles.json. Prescribe a method for application configuration, but allow flexibility in how application configuration is defined.
Register components in an imperitive fashion, see angularApp.factory, angularApp.controller, etc. Alternatively, implement our own application object with new registries and it's own form of registering objects.
## Get rid of "extension category" concept.
The concept of an "extension category" is itself an extraneous concept-- an extra layer of interface depth, an extra thing to learn before you can say "hello world". Extension points should be clearly supported and documented with whatever interfaces make sense. Developers who wish to add something that is conceptually equivalent to an extension category can do so directly, in the manner that suites their needs, without us forcing a common method on them.
## Reduce number and depth of extension points
Clearly specify supported extension points (e.g. persistence, model providers, telemetry providers, routes, time systems), but don't claim that the system has a clear and perfect repeatable solution for unknown extension types. New extension categories can be implemented in whatever way makes sense, without prescribing "the one and only system for managing extensions".
The underlying problem here is we are predicting needs for extension points where none exist-- if we try and design the extension system before we know how it is used, we design the wrong thing and have to rewrite it later.
## Composite services should not be the default
Understanding composite services, and describing services as composite services can confuse developers. Aggregators are implemented once and forgotten, while decorators tend to be hacky, brittle solutions that are generally needed to avoid circular imports. While composite services are a useful construct, it reduces interface depth to implement them as registries + typed providers.
You can write a provider (provides "thing x" for "inputs y") with a simple interface. A provider has two or more methods:
* a method which takes "inputs y" and returns True if it knows how to provide "thing x", false otherwise.
* one or more methods which provide "thing x" for objects of "inputs y".
Actually checking whether a provider can respond to a request before asking it to do work allows for faster failure and clearer errors when no providers match the request.
## Get rid of views, representations, and templates.
Templates are an implementation detail that should be handled by module loaders. Views and representations become "components," and a new concept, "routes", is used to exposing specific views to end users.
`components` - building blocks for views, have clear inputs and outputs, and can be coupled to other components when it makes sense. (e.g. parent-child components such as menu and menu item), but should have ZERO knowledge of our data models or telemetry apis. They should define data models that enable them to do their job well while still being easy to test.
`routes` - a view type for a given domain object, e.g. a plot, table, display layout, etc. Can be described as "whatever shows in the main screen when you are viewing an object." Handle loading of data from a domain object and passing that data to the view components. Routes should support editing as it makes sense in their own context.
To facilitate testing:
* routes should be testable without having to test the actual view.
* components should be independently testable with zero knowledge of our data models or telemetry APIs.
Component code should be organized side by side, such as:
```
app
|- components
|- productDetail
| |- productDetail.js
| |- productDetail.css
| |- productDetail.html
| |- productDetailSpec.js
|- productList
|- checkout
|- wishlist
```
Components are not always reusable, and we shouldn't be overly concerned with making them so. If components are heavily reused, they should either be moved to a platform feature (e.g. notifications, indicators), or broken off as an external dependency (e.g. publish mct-plot as mct-plot.js).
# Reducing interface depth (The angular discussion)
Two options here: use more angular, use less angular. Wrapping angular methods does not reduce interface depth and must be avoided.
The primary issue with angular is duplications of concerns-- both angular and the openmctweb platform implement the same tools side by side and it can be hard to comprehend-- it increases interface depth. For other concerns, see footnotes[1].
Wrapping angular methods for non-view related code is confusing to developers because of the random constraints angular places on these items-- developers ultimately have to understand both angular DI and our framework. For example, it's not possible to name the topic service "topicService" because angular expects Services to be implemented by Providers, which is different than our expectation.
To reduce interface depth, we can replace our own provider and registry patterns with angular patterns, or we can only utilize angular view logic, and only use our own DI patterns.
## More angular: for all services
Increasing our commitment to angular would mean using more of the angular factorys, services, etc, and less of our home grown tools. We'd implement our services and extension points as angular providers, and make them configurable via app.config.
As an example, registering a specific type of model provider in angular would look like:
```javascript
mct.provider('model', modelProvider() { /* implementation */});
mct.config(['modelProvider', function (modelProvider) {
modelProvider.providers.push(RootModelProvider);
}]);
```
## Less angular: only for views
If we wish to use less angular, I would recommend discontinuing use of all angular components that are not view related-- services, factories, $http, etc, and implementing them in our own paradigm. Otherwise, we end up with layered interfaces-- one of the goals we would like to avoid.
# Standard packaging and build system
Standardize the packaging and build system, and completely separate the core platform from deployments. Prescribe a starting point for deployments, but allow flexibility.
## Use systemjs for module loading
Allow developers to use whatever module loading system they'd like to use, while still supporting all standard cases. We should also use this system for loading assets (css, scss, html templates), which makes it easier to implement a single file deployment using standard build tooling.
## Use gulp or grunt for standard tooling
Using gulp or grunt as a task runner would bring us in line with standard web developer workflows and help standardize rendering, deployment, and packaging. Additional tools can be added to the workflow at low cost, simplifying the set up of developer environments.
Gulp and grunt provide useful developer tooling such as live reload, automatic scss/less/etc compiliation, and ease of extensibility for standard production build processes. They're key in decoupling code.
## Package openmctweb as single versioned file.
Deployments should depend on a specific version of openmctweb, but otherwise be allowed to have their own deployment and development toolsets.
Customizations and deployments of openmctweb should not use the same build tooling as the core platform; instead they should be free to use their own build tools as they wish. (We would provide a template for an application, based on our experience with warp-for-rp and vista)
Installation and utilization of openmctweb should be as simple as downloading the js file, including it in your own html page, and then initializing an app and running it. If a developer would prefer, they could use bower or npm to handle installation.
Then, if we're using imperative methods for extending the application we can use the following for basic customization:
```html
<script src="//localhost/openmctweb.js"></script>
<script>
// can configure from object
var myApp = new OpenMCTWeb({
persitence: {
providers: [
{
type: 'elastic',
uri: 'http://someElasticHost/'
} // ...
]
}
});
// alternative configurations
myApp.persistence.addProvider(MyPersistenceAdapter);
myApp.model.addProvider(someProviderObject);
// Removing via method
myApp.persistence.removeProvider('some method for removing functionality');
// directly mutating providers
myApp.persistence.providers = [ThisProviderStandsAlone];
//
myApp.run();
</script>
```
This packaging reduces the complexity of managing multiple deployed versions, and also allows us to provide users with incredibly simple tutorials-- they can use whatever tooling they like. For instance, a hello world tutorial may take the option of "exposing a new object in the tree".
```javascript
var myApp = new OpenMCTWeb();
myApp.roots.addRoot({
id: 'myRoot',
name: 'Hello World!',
});
myApp.routes.setDefault('myRoot');
myApp.run();
```
# Misc Improvements
## Refresh on navigation
In cases where navigation events change the entire screen, we should be using routes and location changes to navigate between objects. We should be using href for all navigation events.
At the same time, navigating should refresh state of every visible object. A properly configured persistence store will handle caching with standard cache headers and 304 not modified responses, which will provide good performance of object reloads, while helping us ensure that objects are always in sync between clients.
View state (say, the expanded tree nodes) should not be tied to caching of data-- it should be something we intentionally persist and restore with each navigation. Data (such as object definitions) should be reloaded from server as necessary to restore state.
## Move persistence adapter to promise rejection.
Simple: reject on fail, resolve on success.
## Remove bulk requests from providers
Aggregators can request multiple things at once, but individual providers should only have to implement handling at the level of a single request. Each provider can implement it's own internal batching, but it should support making requests at a finer level of detail.
Excessive wrapping of code with $q.all causes additional digest cycles and decreased performance.
For example, instead of every telemetry provider responding to a given telemetry request, aggregators should route each request to the first provider that can fulfill that request.
# Notes on current API proposals:
* [RequireJS for Dependency Injection](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#requirejs-as-dependency-injector): requires other topics to be discussed first.
* [Arbitrary HTML Views](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#arbitrary-html-views): think there is a place for it, requires other topics to be discussed first.
* [Wrap Angular Services](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#wrap-angular-services): No, this is bad.
* [Bundle definitions in Javascript](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#bundle-declarations-in-javascript): Points to a solution, but ultimately requires more discussion.
* [pass around a dependency injector](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#pass-around-a-dependency-injector): No.
* [remove partial constructors](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#remove-partial-constructors): Yes, this should be superseded by another proposal though. The entire concept was a messy solution to dependency injection issues caused by declarative syntax.
* [Rename views to applications](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#rename-views-to-applications): Points to a problem that needs to be solved but I think the name is bad.
* [Provide classes for extensions](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#provide-classes-for-extensions): Yes, in specific places
* [Normalize naming conventions](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#normalize-naming-conventions): Yes.
* [Expose no third-party APIs](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#expose-no-third-party-apis): Completely disagree, points to a real problem with poor angular integration.
* [Register Extensions as Instances instead of Constructors](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#register-extensions-as-instances-instead-of-constructors): Superseded by the fact that we should not hope to implement a generic construct.
* [Remove capability delegation](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#remove-capability-delegation): Yes.
* [Nomenclature Change](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#nomenclature-change): Yes, hope to discuss the implications of this more clearly in other proposals.
* [Capabilities as mixins](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#capabilities-as-mixins): Yes.
* [Remove appliesTo methods](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#remove-applies-to-methods): No-- I think some level of this is necessary. I think a more holistic approach to policy is needed. it's a rather complicated system.
* [Revise telemetry API](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#revise-telemetry-api): If we can rough out and agree to the specifics, then Yes. Needs discussion.
* [Allow composite services to fail gracefully](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#allow-composite-services-to-fail-gracefully): No. As mentioned above, I think composite services themselves should be eliminated for a more purpose bound tool.
* [Plugins as angular modules](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#plugins-as-angular-modules): Should we decide to embrace Angular completely, I would support this. Otherwise, no.
* [Contextual Injection](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#contextual-injection): No, don't see a need.
* [Add New Abstractions for Actions](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#add-new-abstractions-for-actions): Worth a discussion.
* [Add gesture handlers](https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign.md#add-gesture-handlers): Yes if we can agree on details. We need a platform implementation that is easy to use, but we should not reinvent the wheel.
# [1] Footnote: The angular debacle
## "Do or do not, there is no try"
A commonly voiced concern of embracing angular is the possibility of becoming dependent on a third party framework. This concern is itself detrimental-- if we're afraid of becoming dependent on a third party framework, then we will do a bad job of using the framework, and inevitably will want to stop using it.
If we're using a framework, we need to use it fully, or not use it at all.
## A lack of commitment
A number of the concerns we heard from developers and interns can be attributed to the tenuous relationship between the OpenMCTWeb platform and angular. We claimed to be angular, but we weren't really angular. Instead, we are caught between our incomplete framework paradigm and the angular paradigm. In many cases we reinvented the wheel or worked around functionality that angular provides, and ended up in a more confusing state.
## Commitment is good!
We could just be an application that is built with angular.
An application that is modular and extensible not because it reinvents tools for providing modularity and extensibility, but because it reuses existing tools for modularity and extensibility.
There are benefits to buying into the angular paradigm: shift documentation burden to external project, engage a larger talent pool available both as voluntary open source contributors and as experienced developers for hire, and gain access to an ecosystem of tools that we can use to increase the speed of development.
There are negatives too: Angular is a monolith, it has performance concerns, and an unclear future. If we can't live with it, we should look at alternatives.

View File

@ -0,0 +1,164 @@
# Imperative Plugins
This is a design proposal for handling
[bundle declarations in JavaScript](
APIRedesign.md#bundle-declarations-in-javascript).
## Developer Use Cases
Developers will want to use bundles/plugins to (in rough order
of occurrence):
1. Add new extension instances.
2. Use existing services
3. Add new service implementations.
4. Decorate service implementations.
5. Decorate extension instances.
6. Add new types of services.
7. Add new extension categories.
Notably, bullets 4 and 5 above are currently handled implicitly,
which has been cited as a source of confusion.
## Interfaces
Two base classes may be used to satisfy these use cases:
* The `CompositeServiceFactory` provides composite service instances.
Decorators may be added; the approach used for compositing may be
modified; and individual services may be registered to support compositing.
* The `ExtensionRegistry` allows for the simpler case where what is desired
is an array of all instances of some kind of thing within the system.
Note that additional developer use cases may be supported by using the
more general-purpose `Registry`
```nomnoml
[Factory.<T, V>
|
- factoryFn : function (V) : T
|
+ decorate(decoratorFn : function (T, V) : T, options? : RegistrationOptions)
]-:>[function (V) : T]
[RegistrationOptions |
+ priority : number or string
]
[Registry.<T, V>
|
- compositorFn : function (Array.<T>) : V
|
+ register(item : T, options? : RegistrationOptions)
+ composite(compositorFn : function (Array.<T>) : V, options? : RegistrationOptions)
]-:>[Factory.<V, Void>]
[Factory.<V, Void>]-:>[Factory.<T, V>]
[ExtensionRegistry.<T>]-:>[Registry.<T, Array.<T>>]
[Registry.<T, Array.<T>>]-:>[Registry.<T, V>]
[CompositeServiceFactory.<T>]-:>[Registry.<T, T>]
[Registry.<T, T>]-:>[Registry.<T, V>]
```
## Examples
### 1. Add new extension instances.
```js
// Instance-style registration
mct.types.register(new mct.Type({
key: "timeline",
name: "Timeline",
description: "A container for activities ordered in time."
});
// Factory-style registration
mct.actions.register(function (domainObject) {
return new RemoveAction(domainObject);
}, { priority: 200 });
```
### 2. Use existing services
```js
mct.actions.register(function (domainObject) {
var dialogService = mct.ui.dialogServiceFactory();
return new PropertiesAction(dialogService, domainObject);
});
```
### 3. Add new service implementations
```js
// Instance-style registration
mct.persistenceServiceFactory.register(new LocalPersistenceService());
// Factory-style registration
mct.persistenceServiceFactory.register(function () {
var $http = angular.injector(['ng']).get('$http');
return new LocalPersistenceService($http);
});
```
### 4. Decorate service implementations
```js
mct.modelServiceFactory.decorate(function (modelService) {
return new CachingModelDecorator(modelService);
}, { priority: 100 });
```
### 5. Decorate extension instances
```js
mct.capabilities.decorate(function (capabilities) {
return capabilities.map(decorateIfApplicable);
});
```
This use case is not well-supported by these API changes. The most
common case for decoration is capabilities, which are under reconsideration;
should consider handling decoration of capabilities in a different way.
### 6. Add new types of services
```js
myModule.myServiceFactory = new mct.CompositeServiceFactory();
// In cases where a custom composition strategy is desired
myModule.myServiceFactory.composite(function (services) {
return new MyServiceCompositor(services);
});
```
### 7. Add new extension categories.
```js
myModule.hamburgers = new mct.ExtensionRegistry();
```
## Evaluation
### Benefits
* Encourages separation of registration from declaration (individual
components are decoupled from the manner in which they are added
to the architecture.)
* Minimizes "magic." Dependencies are acquired, managed, and exposed
using plain-old-JavaScript without any dependency injector present
to obfuscate what is happening.
* Offers comparable expressive power to existing APIs; can still
extend the behavior of platform components in a variety of ways.
* Does not force or limit formalisms to use;
### Detriments
* Does not encourage separation of dependency acquisition from
declaration; that is, it would be quite natural using this API
to acquire references to services during the constructor call
to an extension or service. But, passing these in as constructor
arguments is preferred (to separate implementation from architecture.)
* Adds (negligible?) boilerplate relative to declarative syntax.
* Relies on factories, increasing number of interfaces to be concerned
with.

View File

@ -0,0 +1,138 @@
# Roles
Roles are presented as an alternative formulation to capabilities
(dynamic behavior associated with individual domain objects.)
Specific goals here:
* Dependencies of individual scripts should be clear.
* Domain objects should be able to selectively exhibit a wide
variety of behaviors.
## Developer Use Cases
1. Checking for the existence of behavior.
2. Using behavior.
3. Augmenting existing behaviors.
4. Overriding existing behaviors.
5. Adding new behaviors.
## Overview of Proposed Solution
Remove `getCapability` from domain objects; add roles as external
services which can be applied to domain objects.
## Interfaces
```nomnoml
[Factory.<T, V>
|
- factoryFn : function (V) : T
|
+ decorate(decoratorFn : function (T, V) : T, options? : RegistrationOptions)
]-:>[function (V) : T]
[RegistrationOptions |
+ priority : number or string
]<:-[RoleOptions |
+ validate : function (DomainObject) : boolean
]
[Role.<T> |
+ validate(domainObject : DomainObject) : boolean
+ decorate(decoratorFn : function (T, V) : T, options? : RoleOptions)
]-:>[Factory.<T, DomainObject>]
[Factory.<T, DomainObject>]-:>[Factory.<T, V>]
```
## Examples
### 1. Checking for the existence of behavior
```js
function PlotViewPolicy(telemetryRole) {
this.telemetryRole = telemetryRole;
}
PlotViewPolicy.prototype.allow = function (view, domainObject) {
return this.telemetryRole.validate(domainObject);
};
```
### 2. Using behavior
```js
PropertiesAction.prototype.perform = function () {
var mutation = this.mutationRole(this.domainObject);
return this.showDialog.then(function (newModel) {
return mutation.mutate(function () {
return newModel;
});
});
};
```
### 3. Augmenting existing behaviors
```js
// Non-Angular style
mct.roles.persistenceRole.decorate(function (persistence) {
return new DecoratedPersistence(persistence);
});
// Angular style
myModule.decorate('persistenceRole', ['$delegate', function ($delegate) {
return new DecoratedPersistence(persistence);
}]);
```
### 4. Overriding existing behaviors
```js
// Non-Angular style
mct.roles.persistenceRole.decorate(function (persistence, domainObject) {
return domainObject.getModel().type === 'someType' ?
new DifferentPersistence(domainObject) :
persistence;
}, {
validate: function (domainObject, next) {
return domainObject.getModel().type === 'someType' || next();
}
});
```
### 5. Adding new behaviors
```js
function FooRole() {
mct.Role.apply(this, [function (domainObject) {
return new Foo(domainObject);
}]);
}
FooRole.prototype = Object.create(mct.Role.prototype);
FooRole.prototype.validate = function (domainObject) {
return domainObject.getModel().type === 'some-type';
};
//
myModule.roles.fooRole = new FooRole();
```
## Evaluation
### Benefits
* Simplifies/standardizes augmentation or replacement of behavior associated
with specific domain objects.
* Minimizes number of abstractions; roles are just factories.
* Clarifies dependencies; roles used must be declared/acquired in the
same manner as services.
### Detriments
* Externalizes functionality which is conceptually associated with a
domain object.
* Relies on factories, increasing number of interfaces to be concerned
with.

View File

@ -37,7 +37,6 @@ define([
"./src/controllers/ViewSwitcherController",
"./src/controllers/BottomBarController",
"./src/controllers/GetterSetterController",
"./src/controllers/SplitPaneController",
"./src/controllers/SelectorController",
"./src/controllers/ObjectInspectorController",
"./src/controllers/BannerController",
@ -66,7 +65,6 @@ define([
ViewSwitcherController,
BottomBarController,
GetterSetterController,
SplitPaneController,
SelectorController,
ObjectInspectorController,
BannerController,
@ -241,10 +239,6 @@ define([
"$scope"
]
},
{
"key": "SplitPaneController",
"implementation": SplitPaneController
},
{
"key": "SelectorController",
"implementation": SelectorController,

View File

@ -1,30 +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.
-->
<span ng-controller="SplitPaneController as splitter">
<div class="splitter" ng-style="splitter.style()"
mct-drag="splitter.move(delta.x)">
</div>
<div class='split-pane-component items pane' style="right:0;"
ng-style="splitter.style()"
ng-transclude>
</div>
</span>

View File

@ -1,89 +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.
*****************************************************************************/
/*global define*/
define(
[],
function () {
"use strict";
var DEFAULT_MAXIMUM = 1000,
DEFAULT_MINIMUM = 120;
/**
* Controller for the splitter in Browse mode. Current implementation
* uses many hard-coded constants; this could be generalized.
* @memberof platform/commonUI/general
* @constructor
*/
function SplitPaneController() {
this.current = 200;
this.start = 200;
this.assigned = false;
}
/**
* Get the current position of the splitter, in pixels
* from the left edge.
* @returns {number} position of the splitter, in pixels
*/
SplitPaneController.prototype.state = function (defaultState) {
// Set the state to the desired default, if we don't have a
// "real" current state yet.
if (arguments.length > 0 && !this.assigned) {
this.current = defaultState;
this.assigned = true;
}
return this.current;
};
/**
* Begin moving the splitter; this will note the splitter's
* current position, which is necessary for correct
* interpretation of deltas provided by mct-drag.
*/
SplitPaneController.prototype.startMove = function () {
this.start = this.current;
};
/**
* Move the splitter a number of pixels to the right
* (negative numbers move the splitter to the left.)
* This movement is relative to the position of the
* splitter when startMove was last invoked.
* @param {number} delta number of pixels to move
*/
SplitPaneController.prototype.move = function (delta, minimum, maximum) {
// Ensure defaults for minimum/maximum
maximum = isNaN(maximum) ? DEFAULT_MAXIMUM : maximum;
minimum = isNaN(minimum) ? DEFAULT_MINIMUM : minimum;
// Update current splitter state
this.current = Math.min(
maximum,
Math.max(minimum, this.start + delta)
);
};
return SplitPaneController;
}
);

View File

@ -22,8 +22,8 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/DateTimePickerController"],
function (DateTimePickerController) {
["../../src/controllers/DateTimePickerController", "moment"],
function (DateTimePickerController, moment) {
"use strict";
describe("The DateTimePickerController", function () {
@ -39,6 +39,14 @@ define(
});
}
function fireWatchCollection(expr, value) {
mockScope.$watchCollection.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
@ -57,6 +65,131 @@ define(
);
});
it("updates value in model when values in scope change", function () {
mockScope.date = {
year: 1998,
month: 0,
day: 6
};
mockScope.time = {
hours: 12,
minutes: 34,
seconds: 56
};
fireWatchCollection("date", mockScope.date);
expect(mockScope.ngModel[mockScope.field])
.toEqual(moment.utc("1998-01-06 12:34:56").valueOf());
});
describe("once initialized with model state", function () {
var testTime = moment.utc("1998-01-06 12:34:56").valueOf();
beforeEach(function () {
fireWatch("ngModel[field]", testTime);
});
it("exposes date/time values in scope", function () {
expect(mockScope.date.year).toEqual(1998);
expect(mockScope.date.month).toEqual(0); // Months are zero-indexed
expect(mockScope.date.day).toEqual(6);
expect(mockScope.time.hours).toEqual(12);
expect(mockScope.time.minutes).toEqual(34);
expect(mockScope.time.seconds).toEqual(56);
});
it("provides names for time properties", function () {
Object.keys(mockScope.time).forEach(function (key) {
expect(mockScope.nameFor(key))
.toEqual(jasmine.any(String));
});
});
it("provides options for time properties", function () {
Object.keys(mockScope.time).forEach(function (key) {
expect(mockScope.optionsFor(key))
.toEqual(jasmine.any(Array));
});
});
it("exposes times to populate calendar as a table", function () {
// Verify that data structure is as expected by template
expect(mockScope.table).toEqual(jasmine.any(Array));
expect(mockScope.table[0]).toEqual(jasmine.any(Array));
expect(mockScope.table[0][0]).toEqual({
year: jasmine.any(Number),
month: jasmine.any(Number),
day: jasmine.any(Number),
dayOfYear: jasmine.any(Number)
});
});
it("contains the current date in its initial table", function () {
var matchingCell;
// Should be able to find the selected date
mockScope.table.forEach(function (row) {
row.forEach(function (cell) {
if (cell.dayOfYear === 6) {
matchingCell = cell;
}
});
});
expect(matchingCell).toEqual({
year: 1998,
month: 0,
day: 6,
dayOfYear: 6
});
});
it("allows the displayed month to be advanced", function () {
// Around the edges of the displayed calendar we
// may be in previous or subsequent month, so
// test around the middle.
var i, originalMonth = mockScope.table[2][0].month;
function mod12(month) {
return ((month % 12) + 12) % 12;
}
for (i = 1; i <= 12; i += 1) {
mockScope.changeMonth(1);
expect(mockScope.table[2][0].month)
.toEqual(mod12(originalMonth + i));
}
for (i = 11; i >= -12; i -= 1) {
mockScope.changeMonth(-1);
expect(mockScope.table[2][0].month)
.toEqual(mod12(originalMonth + i));
}
});
it("allows checking if a cell is in the current month", function () {
expect(mockScope.isInCurrentMonth(mockScope.table[2][0]))
.toBe(true);
});
it("allows cells to be selected", function () {
mockScope.select(mockScope.table[2][0]);
expect(mockScope.isSelected(mockScope.table[2][0]))
.toBe(true);
mockScope.select(mockScope.table[2][1]);
expect(mockScope.isSelected(mockScope.table[2][0]))
.toBe(false);
expect(mockScope.isSelected(mockScope.table[2][1]))
.toBe(true);
});
it("allows cells to be compared", function () {
var table = mockScope.table;
expect(mockScope.dateEquals(table[2][0], table[2][1]))
.toBe(false);
expect(mockScope.dateEquals(table[2][1], table[2][1]))
.toBe(true);
});
});
});
}

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.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/SplitPaneController"],
function (SplitPaneController) {
"use strict";
describe("The split pane controller", function () {
var controller;
beforeEach(function () {
controller = new SplitPaneController();
});
it("has an initial position", function () {
expect(controller.state() > 0).toBeTruthy();
});
it("can be moved", function () {
var initialState = controller.state();
controller.startMove();
controller.move(50);
expect(controller.state()).toEqual(initialState + 50);
});
it("clamps its position", function () {
var initialState = controller.state();
controller.startMove();
// Move some really extreme number
controller.move(-100000);
// Shouldn't have moved below 0...
expect(controller.state() > 0).toBeTruthy();
// ...but should have moved left somewhere
expect(controller.state() < initialState).toBeTruthy();
// Then do the same to the right
controller.move(100000);
// Shouldn't have moved below 0...
expect(controller.state() < 100000).toBeTruthy();
// ...but should have moved left somewhere
expect(controller.state() > initialState).toBeTruthy();
});
it("accepts a default state", function () {
// Should use default state the first time...
expect(controller.state(12321)).toEqual(12321);
// ...but not after it's been initialized
expect(controller.state(42)).toEqual(12321);
});
});
}
);

View File

@ -34,14 +34,14 @@ define(
mockElement,
testAttrs,
mockBody,
mockParentEl,
mockPlainEl,
testRect,
mctClickElsewhere;
function testEvent(x, y) {
return {
pageX: x,
pageY: y,
clientX: x,
clientY: y,
preventDefault: jasmine.createSpy("preventDefault")
};
}
@ -55,8 +55,8 @@ define(
jasmine.createSpyObj("element", JQLITE_METHODS);
mockBody =
jasmine.createSpyObj("body", JQLITE_METHODS);
mockParentEl =
jasmine.createSpyObj("parent", ["getBoundingClientRect"]);
mockPlainEl =
jasmine.createSpyObj("htmlElement", ["getBoundingClientRect"]);
testAttrs = {
mctClickElsewhere: "some Angular expression"
@ -67,6 +67,8 @@ define(
width: 60,
height: 75
};
mockElement[0] = mockPlainEl;
mockPlainEl.getBoundingClientRect.andReturn(testRect);
mockDocument.find.andReturn(mockBody);
@ -78,6 +80,49 @@ define(
expect(mctClickElsewhere.restrict).toEqual("A");
});
it("detaches listeners when destroyed", function () {
expect(mockBody.off).not.toHaveBeenCalled();
mockScope.$on.calls.forEach(function (call) {
if (call.args[0] === '$destroy') {
call.args[1]();
}
});
expect(mockBody.off).toHaveBeenCalled();
expect(mockBody.off.mostRecentCall.args)
.toEqual(mockBody.on.mostRecentCall.args);
});
it("listens for mousedown on the document's body", function () {
expect(mockBody.on)
.toHaveBeenCalledWith('mousedown', jasmine.any(Function));
});
describe("when a click occurs outside the element's bounds", function () {
beforeEach(function () {
mockBody.on.mostRecentCall.args[1](testEvent(
testRect.left + testRect.width + 10,
testRect.top + testRect.height + 10
));
});
it("triggers an evaluation of its related Angular expression", function () {
expect(mockScope.$eval)
.toHaveBeenCalledWith(testAttrs.mctClickElsewhere);
});
});
describe("when a click occurs within the element's bounds", function () {
beforeEach(function () {
mockBody.on.mostRecentCall.args[1](testEvent(
testRect.left + testRect.width / 2,
testRect.top + testRect.height / 2
));
});
it("triggers no evaluation", function () {
expect(mockScope.$eval).not.toHaveBeenCalled();
});
});
});
}

View File

@ -30,13 +30,16 @@ define(
'on',
'addClass',
'children',
'eq'
'eq',
'toggleClass',
'css'
];
describe("The mct-split-pane directive", function () {
var mockParse,
mockLog,
mockInterval,
mockParsed,
mctSplitPane;
beforeEach(function () {
@ -45,6 +48,11 @@ define(
jasmine.createSpyObj('$log', ['warn', 'info', 'debug']);
mockInterval = jasmine.createSpy('$interval');
mockInterval.cancel = jasmine.createSpy('mockCancel');
mockParsed = jasmine.createSpy('parsed');
mockParsed.assign = jasmine.createSpy('assign');
mockParse.andReturn(mockParsed);
mctSplitPane = new MCTSplitPane(
mockParse,
mockLog,
@ -61,8 +69,19 @@ define(
mockElement,
testAttrs,
mockChildren,
mockFirstPane,
mockSplitter,
mockSecondPane,
controller;
function fireOn(eventType) {
mockScope.$on.calls.forEach(function (call) {
if (call.args[0] === eventType) {
call.args[1]();
}
});
}
beforeEach(function () {
mockScope =
jasmine.createSpyObj('$scope', ['$apply', '$watch', '$on']);
@ -71,10 +90,33 @@ define(
testAttrs = {};
mockChildren =
jasmine.createSpyObj('children', JQLITE_METHODS);
mockFirstPane =
jasmine.createSpyObj('firstPane', JQLITE_METHODS);
mockSplitter =
jasmine.createSpyObj('splitter', JQLITE_METHODS);
mockSecondPane =
jasmine.createSpyObj('secondPane', JQLITE_METHODS);
mockElement.children.andReturn(mockChildren);
mockChildren.eq.andReturn(mockChildren);
mockChildren[0] = {};
mockElement[0] = {
offsetWidth: 12321,
offsetHeight: 45654
};
mockChildren.eq.andCallFake(function (i) {
return [mockFirstPane, mockSplitter, mockSecondPane][i];
});
mockFirstPane[0] = { offsetWidth: 123, offsetHeight: 456 };
mockSplitter[0] = {
nodeName: 'mct-splitter',
offsetWidth: 10,
offsetHeight: 456
};
mockSecondPane[0] = { offsetWidth: 10, offsetHeight: 456 };
mockChildren[0] = mockFirstPane[0];
mockChildren[1] = mockSplitter[0];
mockChildren[3] = mockSecondPane[0];
mockChildren.length = 3;
controller = mctSplitPane.controller[3](
mockScope,
@ -87,6 +129,77 @@ define(
expect(mockInterval.mostRecentCall.args[3]).toBe(false);
});
it("exposes its splitter's initial position", function () {
expect(controller.position()).toEqual(
mockFirstPane[0].offsetWidth + mockSplitter[0].offsetWidth
);
});
it("exposes the current anchoring mode", function () {
expect(controller.anchor()).toEqual({
edge : 'left',
opposite : 'right',
dimension : 'width',
orientation : 'vertical'
});
});
it("allows classes to be toggled on contained elements", function () {
controller.toggleClass('resizing');
expect(mockChildren.toggleClass)
.toHaveBeenCalledWith('resizing');
});
it("allows positions to be set", function () {
var testValue = mockChildren[0].offsetWidth + 50;
controller.position(testValue);
expect(mockFirstPane.css).toHaveBeenCalledWith(
'width',
(testValue - mockSplitter[0].offsetWidth) + 'px'
);
});
it("issues no warnings under nominal usage", function () {
expect(mockLog.warn).not.toHaveBeenCalled();
});
it("warns if no mct-splitter is present", function () {
mockSplitter[0].nodeName = "not-mct-splitter";
controller = mctSplitPane.controller[3](
mockScope,
mockElement,
testAttrs
);
expect(mockLog.warn).toHaveBeenCalled();
});
it("warns if an unknown anchor key is given", function () {
testAttrs.anchor = "middle";
controller = mctSplitPane.controller[3](
mockScope,
mockElement,
testAttrs
);
expect(mockLog.warn).toHaveBeenCalled();
});
it("updates positions on a timer", function () {
mockFirstPane[0].offsetWidth += 100;
// Should not reflect the change yet
expect(controller.position()).not.toEqual(
mockFirstPane[0].offsetWidth + mockSplitter[0].offsetWidth
);
mockInterval.mostRecentCall.args[0]();
expect(controller.position()).toEqual(
mockFirstPane[0].offsetWidth + mockSplitter[0].offsetWidth
);
});
it("cancels the active interval when scope is destroyed", function () {
expect(mockInterval.cancel).not.toHaveBeenCalled();
fireOn('$destroy');
expect(mockInterval.cancel).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,113 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/directives/MCTSplitter"],
function (MCTSplitter) {
'use strict';
describe("The mct-splitter directive", function () {
var mctSplitter;
beforeEach(function () {
mctSplitter = new MCTSplitter();
});
it("is applicable to elements", function () {
expect(mctSplitter.restrict).toEqual("E");
});
it("depends on the mct-split-pane controller", function () {
expect(mctSplitter.require).toEqual("^mctSplitPane");
});
describe("when linked", function () {
var mockScope,
mockElement,
testAttrs,
mockSplitPane;
beforeEach(function () {
mockScope = jasmine.createSpyObj(
'$scope',
[ '$on', '$watch' ]
);
mockElement = jasmine.createSpyObj(
'element',
[ 'addClass' ]
);
testAttrs = {};
mockSplitPane = jasmine.createSpyObj(
'mctSplitPane',
[ 'position', 'toggleClass', 'anchor' ]
);
mctSplitter.link(
mockScope,
mockElement,
testAttrs,
mockSplitPane
);
});
it("adds a splitter class", function () {
expect(mockElement.addClass)
.toHaveBeenCalledWith('splitter');
});
describe("and then manipulated", function () {
var testPosition;
beforeEach(function () {
testPosition = 12321;
mockSplitPane.position.andReturn(testPosition);
mockSplitPane.anchor.andReturn({
orientation: 'vertical',
reversed: false
});
mockScope.splitter.startMove();
});
it("adds a 'resizing' class", function () {
expect(mockSplitPane.toggleClass)
.toHaveBeenCalledWith('resizing');
});
it("repositions during drag", function () {
mockScope.splitter.move([ 10, 0 ]);
expect(mockSplitPane.position)
.toHaveBeenCalledWith(testPosition + 10);
});
it("removes the 'resizing' class when finished", function () {
mockSplitPane.toggleClass.reset();
mockScope.splitter.endMove();
expect(mockSplitPane.toggleClass)
.toHaveBeenCalledWith('resizing');
});
});
});
});
}
);

View File

@ -8,7 +8,6 @@
"controllers/GetterSetterController",
"controllers/ObjectInspectorController",
"controllers/SelectorController",
"controllers/SplitPaneController",
"controllers/TimeRangeController",
"controllers/ToggleController",
"controllers/TreeNodeController",
@ -20,6 +19,7 @@
"directives/MCTResize",
"directives/MCTScroll",
"directives/MCTSplitPane",
"directives/MCTSplitter",
"services/Popup",
"services/PopupService",
"services/UrlService",

View File

@ -35,10 +35,21 @@ define(
* Both "Start" and "Restart" share this implementation, but
* control their visibility with different `appliesTo` behavior.
*
* @implements Action
* @implements {Action}
* @memberof platform/features/clock
* @constructor
* @param {Function} now a function which returns the current
* time (typically wrapping `Date.now`)
* @param {ActionContext} context the context for this action
*/
function AbstractStartTimerAction(now, context) {
var domainObject = context.domainObject;
this.domainObject = context.domainObject;
this.now = now;
}
AbstractStartTimerAction.prototype.perform = function () {
var domainObject = this.domainObject,
now = this.now;
function doPersist() {
var persistence = domainObject.getCapability('persistence');
@ -49,13 +60,9 @@ define(
model.timestamp = now();
}
return {
perform: function () {
return domainObject.useCapability('mutation', setTimestamp)
.then(doPersist);
}
};
}
return AbstractStartTimerAction;
}

View File

@ -31,12 +31,22 @@ define(
*
* Behaves the same as (and delegates functionality to)
* the "Start" action.
* @implements Action
*
* @extends {platform/features/clock.AbstractTimerAction}
* @implements {Action}
* @memberof platform/features/clock
* @constructor
* @param {Function} now a function which returns the current
* time (typically wrapping `Date.now`)
* @param {ActionContext} context the context for this action
*/
function RestartTimerAction(now, context) {
return new AbstractStartTimerAction(now, context);
AbstractStartTimerAction.apply(this, [ now, context ]);
}
RestartTimerAction.prototype =
Object.create(AbstractStartTimerAction.prototype);
RestartTimerAction.appliesTo = function (context) {
var model =
(context.domainObject && context.domainObject.getModel())

View File

@ -32,12 +32,21 @@ define(
* Sets the reference timestamp in a timer to the current
* time, such that it begins counting up.
*
* @implements Action
* @extends {platform/features/clock.AbstractTimerAction}
* @implements {Action}
* @memberof platform/features/clock
* @constructor
* @param {Function} now a function which returns the current
* time (typically wrapping `Date.now`)
* @param {ActionContext} context the context for this action
*/
function StartTimerAction(now, context) {
return new AbstractStartTimerAction(now, context);
AbstractStartTimerAction.apply(this, [ now, context ]);
}
StartTimerAction.prototype =
Object.create(AbstractStartTimerAction.prototype);
StartTimerAction.appliesTo = function (context) {
var model =
(context.domainObject && context.domainObject.getModel())

View File

@ -30,19 +30,21 @@ define(
* Controller for views of a Clock domain object.
*
* @constructor
* @memberof platform/features/clock
* @param {angular.Scope} $scope the Angular scope
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
*/
function ClockController($scope, tickerService) {
var text,
ampm,
use24,
lastTimestamp,
var lastTimestamp,
unlisten,
timeFormat;
timeFormat,
self = this;
function update() {
var m = moment.utc(lastTimestamp);
text = timeFormat && m.format(timeFormat);
ampm = m.format("A"); // Just the AM or PM part
self.textValue = timeFormat && m.format(timeFormat);
self.ampmValue = m.format("A"); // Just the AM or PM part
}
function tick(timestamp) {
@ -56,8 +58,8 @@ define(
if (clockFormat !== undefined) {
baseFormat = clockFormat[0];
use24 = clockFormat[1] === 'clock24';
timeFormat = use24 ?
self.use24 = clockFormat[1] === 'clock24';
timeFormat = self.use24 ?
baseFormat.replace('hh', "HH") : baseFormat;
update();
@ -69,31 +71,31 @@ define(
// Listen for clock ticks ... and stop listening on destroy
unlisten = tickerService.listen(tick);
$scope.$on('$destroy', unlisten);
}
return {
/**
* Get the clock's time zone, as displayable text.
* @returns {string}
*/
zone: function () {
ClockController.prototype.zone = function () {
return "UTC";
},
};
/**
* Get the current time, as displayable text.
* @returns {string}
*/
text: function () {
return text;
},
ClockController.prototype.text = function () {
return this.textValue;
};
/**
* Get the text to display to qualify a time as AM or PM.
* @returns {string}
*/
ampm: function () {
return use24 ? '' : ampm;
}
ClockController.prototype.ampm = function () {
return this.use24 ? '' : this.ampmValue;
};
}
return ClockController;
}

View File

@ -31,6 +31,12 @@ define(
*
* This is a short-term workaround to assure Timer views stay
* up-to-date; should be replaced by a global auto-refresh.
*
* @constructor
* @memberof platform/features/clock
* @param {angular.Scope} $scope the Angular scope
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
*/
function RefreshingController($scope, tickerService) {
var unlisten;

View File

@ -33,26 +33,30 @@ define(
* Controller for views of a Timer domain object.
*
* @constructor
* @memberof platform/features/clock
* @param {angular.Scope} $scope the Angular scope
* @param $window Angular-provided window object
* @param {Function} now a function which returns the current
* time (typically wrapping `Date.now`)
*/
function TimerController($scope, $window, now) {
var timerObject,
relevantAction,
sign = '',
text = '',
formatter,
active = true,
relativeTimestamp,
lastTimestamp;
lastTimestamp,
self = this;
function update() {
var timeDelta = lastTimestamp - relativeTimestamp;
if (formatter && !isNaN(timeDelta)) {
text = formatter(timeDelta);
sign = timeDelta < 0 ? "-" : timeDelta >= 1000 ? "+" : "";
self.textValue = formatter(timeDelta);
self.signValue = timeDelta < 0 ? "-" :
timeDelta >= 1000 ? "+" : "";
} else {
text = "";
sign = "";
self.textValue = "";
self.signValue = "";
}
}
@ -75,7 +79,7 @@ define(
updateFormat(formatKey);
updateTimestamp(timestamp);
relevantAction = actionCapability &&
self.relevantAction = actionCapability &&
actionCapability.getActions(actionKey)[0];
update();
@ -92,13 +96,14 @@ define(
}
function tick() {
var lastSign = sign, lastText = text;
var lastSign = self.signValue,
lastText = self.textValue;
lastTimestamp = now();
update();
// We're running in an animation frame, not in a digest cycle.
// We need to trigger a digest cycle if our displayable data
// changes.
if (lastSign !== sign || lastText !== text) {
if (lastSign !== self.signValue || lastText !== self.textValue) {
$scope.$apply();
}
if (active) {
@ -117,50 +122,58 @@ define(
active = false;
});
return {
this.$scope = $scope;
this.signValue = '';
this.textValue = '';
this.updateObject = updateObject;
}
/**
* Get the glyph to display for the start/restart button.
* @returns {string} glyph to display
*/
buttonGlyph: function () {
return relevantAction ?
relevantAction.getMetadata().glyph : "";
},
TimerController.prototype.buttonGlyph = function () {
return this.relevantAction ?
this.relevantAction.getMetadata().glyph : "";
};
/**
* Get the text to show for the start/restart button
* (e.g. in a tooltip)
* @returns {string} name of the action
*/
buttonText: function () {
return relevantAction ?
relevantAction.getMetadata().name : "";
},
TimerController.prototype.buttonText = function () {
return this.relevantAction ?
this.relevantAction.getMetadata().name : "";
};
/**
* Perform the action associated with the start/restart button.
*/
clickButton: function () {
if (relevantAction) {
relevantAction.perform();
updateObject($scope.domainObject);
TimerController.prototype.clickButton = function () {
if (this.relevantAction) {
this.relevantAction.perform();
this.updateObject(this.$scope.domainObject);
}
},
};
/**
* Get the sign (+ or -) of the current timer value, as
* displayable text.
* @returns {string} sign of the current timer value
*/
sign: function () {
return sign;
},
TimerController.prototype.sign = function () {
return this.signValue;
};
/**
* Get the text to display for the current timer value.
* @returns {string} current timer value
*/
text: function () {
return text;
}
TimerController.prototype.text = function () {
return this.textValue;
};
}
return TimerController;
}

View File

@ -37,8 +37,10 @@ define(
* supports `TimerController`.
*
* @constructor
* @memberof platform/features/clock
*/
function TimerFormatter() {
}
// Round this timestamp down to the second boundary
// (e.g. 1124ms goes down to 1000ms, -2400ms goes down to -3000ms)
@ -46,35 +48,27 @@ define(
return Math.abs(Math.floor(duration / 1000) * 1000);
}
// Short-form format, e.g. 02:22:11
function short(duration) {
return moment.duration(toWholeSeconds(duration), 'ms')
.format(SHORT_FORMAT, { trim: false });
}
// Long-form format, e.g. 3d 02:22:11
function long(duration) {
return moment.duration(toWholeSeconds(duration), 'ms')
.format(LONG_FORMAT, { trim: false });
}
return {
/**
* Format a duration for display, using the short form.
* (e.g. 03:33:11)
* @param {number} duration the duration, in milliseconds
* @param {boolean} sign true if positive
*/
short: short,
TimerFormatter.prototype.short = function (duration) {
return moment.duration(toWholeSeconds(duration), 'ms')
.format(SHORT_FORMAT, { trim: false });
};
/**
* Format a duration for display, using the long form.
* (e.g. 0d 03:33:11)
* @param {number} duration the duration, in milliseconds
* @param {boolean} sign true if positive
*/
long: long
TimerFormatter.prototype.long = function (duration) {
return moment.duration(toWholeSeconds(duration), 'ms')
.format(LONG_FORMAT, { trim: false });
};
}
return TimerFormatter;
}

View File

@ -28,31 +28,39 @@ define(
/**
* Indicator that displays the current UTC time in the status area.
* @implements Indicator
* @implements {Indicator}
* @memberof platform/features/clock
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
* @param {string} indicatorFormat format string for timestamps
* shown in this indicator
*/
function ClockIndicator(tickerService, CLOCK_INDICATOR_FORMAT) {
var text = "";
function ClockIndicator(tickerService, indicatorFormat) {
var self = this;
this.text = "";
tickerService.listen(function (timestamp) {
text = moment.utc(timestamp).format(CLOCK_INDICATOR_FORMAT) + " UTC";
self.text = moment.utc(timestamp)
.format(indicatorFormat) + " UTC";
});
return {
getGlyph: function () {
return "C";
},
getGlyphClass: function () {
return "no-icon no-collapse float-right subtle";
},
getText: function () {
return text;
},
getDescription: function () {
return "";
}
ClockIndicator.prototype.getGlyph = function () {
return "C";
};
}
ClockIndicator.prototype.getGlyphClass = function () {
return "no-icon no-collapse float-right subtle";
};
ClockIndicator.prototype.getText = function () {
return this.text;
};
ClockIndicator.prototype.getDescription = function () {
return "";
};
return ClockIndicator;
}

View File

@ -30,23 +30,23 @@ define(
* Calls functions every second, as close to the actual second
* tick as is feasible.
* @constructor
* @memberof platform/features/clock
* @param $timeout Angular's $timeout
* @param {Function} now function to provide the current time in ms
*/
function TickerService($timeout, now) {
var callbacks = [],
last = now() - 1000;
var self = this;
function tick() {
var timestamp = now(),
millis = timestamp % 1000;
// Only update callbacks if a second has actually passed.
if (timestamp >= last + 1000) {
callbacks.forEach(function (callback) {
if (timestamp >= self.last + 1000) {
self.callbacks.forEach(function (callback) {
callback(timestamp);
});
last = timestamp - millis;
self.last = timestamp - millis;
}
// Try to update at exactly the next second
@ -55,35 +55,35 @@ define(
tick();
return {
this.callbacks = [];
this.last = now() - 1000;
}
/**
* Listen for clock ticks. The provided callback will
* be invoked with the current timestamp (in milliseconds
* since Jan 1 1970) at regular intervals, as near to the
* second boundary as possible.
*
* @method listen
* @name TickerService#listen
* @param {Function} callback callback to invoke
* @returns {Function} a function to unregister this listener
*/
listen: function (callback) {
callbacks.push(callback);
TickerService.prototype.listen = function (callback) {
var self = this;
self.callbacks.push(callback);
// Provide immediate feedback
callback(last);
callback(this.last);
// Provide a deregistration function
return function () {
callbacks = callbacks.filter(function (cb) {
self.callbacks = self.callbacks.filter(function (cb) {
return cb !== callback;
});
};
}
};
}
return TickerService;
}
);

View File

@ -424,6 +424,58 @@ define(
expect(controller.selected().style).not.toEqual(oldStyle);
});
describe("on display bounds changes", function () {
var testBounds;
beforeEach(function () {
testBounds = { start: 123, end: 321 };
mockScope.domainObject = mockDomainObject;
mockScope.model = testModel;
findWatch("domainObject")(mockDomainObject);
findWatch("model.modified")(testModel.modified);
findWatch("model.composition")(mockScope.model.composition);
findOn('telemetry:display:bounds')({}, testBounds);
});
it("issues new requests", function () {
expect(mockHandle.request).toHaveBeenCalled();
});
it("requests only a single point", function () {
expect(mockHandle.request.mostRecentCall.args[0].size)
.toEqual(1);
});
describe("and after data has been received", function () {
var mockSeries,
testValue;
beforeEach(function () {
testValue = 12321;
mockSeries = jasmine.createSpyObj('series', [
'getPointCount',
'getDomainValue',
'getRangeValue'
]);
mockSeries.getPointCount.andReturn(1);
mockSeries.getRangeValue.andReturn(testValue);
// Fire the callback associated with the request
mockHandle.request.mostRecentCall.args[1](
mockHandle.getTelemetryObjects()[0],
mockSeries
);
});
it("updates displayed values", function () {
expect(controller.getElements()[0].value)
.toEqual("Formatted " + testValue);
});
});
});
it("reflects limit status", function () {
var elements;
@ -459,6 +511,7 @@ define(
expect(elements[1].cssClass).toEqual("alarm-b");
expect(elements[2].cssClass).toEqual("alarm-c");
});
});
}
);

View File

@ -286,6 +286,34 @@ define(
expect(mockHandle.request.calls.length).toEqual(2);
});
it("maintains externally-provided domain axis bounds after data is received", function () {
mockSeries.getPointCount.andReturn(3);
mockSeries.getRangeValue.andReturn(42);
mockSeries.getDomainValue.andCallFake(function (i) {
return 2500 + i * 2500;
});
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
fireEvent("telemetry:display:bounds", [
{},
{start: 0, end: 10000}
]);
mockHandle.request.mostRecentCall.args[1](
mockDomainObject,
mockSeries
);
// Pan-zoom state should reflect bounds set externally;
// domain axis should not have shrunk to fit data.
expect(
controller.getSubPlots()[0].panZoomStack.getOrigin()[0]
).toEqual(0);
expect(
controller.getSubPlots()[0].panZoomStack.getDimensions()[0]
).toEqual(10000);
});
it("provides classes for legends based on limit state", function () {
var mockTelemetryObjects = mockHandle.getTelemetryObjects();

View File

@ -32,16 +32,14 @@ define(
var TEST_RANGE_VALUE = "some formatted range value";
describe("A range column", function () {
var mockDataSet,
var testDatum,
testMetadata,
mockFormatter,
mockDomainObject,
column;
beforeEach(function () {
mockDataSet = jasmine.createSpyObj(
"data",
[ "getRangeValue" ]
);
testDatum = { testKey: 123, otherKey: 456 };
mockFormatter = jasmine.createSpyObj(
"formatter",
[ "formatDomainValue", "formatRangeValue" ]
@ -50,6 +48,10 @@ define(
key: "testKey",
name: "Test Name"
};
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[ "getModel", "getCapability" ]
);
mockFormatter.formatRangeValue.andReturn(TEST_RANGE_VALUE);
column = new RangeColumn(testMetadata, mockFormatter);
@ -59,20 +61,13 @@ define(
expect(column.getTitle()).toEqual("Test Name");
});
xit("looks up data from a data set", function () {
column.getValue(undefined, mockDataSet, 42);
expect(mockDataSet.getRangeValue)
.toHaveBeenCalledWith(42, "testKey");
});
xit("formats range values as numbers", function () {
mockDataSet.getRangeValue.andReturn(123.45678);
expect(column.getValue(undefined, mockDataSet, 42).text)
it("formats range values as numbers", function () {
expect(column.getValue(mockDomainObject, testDatum).text)
.toEqual(TEST_RANGE_VALUE);
// Make sure that service interactions were as expected
expect(mockFormatter.formatRangeValue)
.toHaveBeenCalledWith(123.45678);
.toHaveBeenCalledWith(testDatum.testKey);
expect(mockFormatter.formatDomainValue)
.not.toHaveBeenCalled();
});

View File

@ -47,7 +47,13 @@ define(
mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
mockSubscription = jasmine.createSpyObj(
'subscription',
['unsubscribe', 'getTelemetryObjects', 'promiseTelemetryObjects']
[
'makeDatum',
'getDatum',
'unsubscribe',
'getTelemetryObjects',
'promiseTelemetryObjects'
]
);
mockDomainObject = jasmine.createSpyObj(
'domainObject',
@ -112,6 +118,20 @@ define(
expect(handle.getSeries(mockDomainObject))
.toEqual(mockSeries);
});
it("provides access to the datum objects by index", function () {
var testDatum = { a: 1, b: 2 }, testIndex = 42;
mockSubscription.makeDatum.andReturn(testDatum);
handle.request({});
expect(handle.getDatum(mockDomainObject, testIndex))
.toEqual(testDatum);
expect(mockSubscription.makeDatum)
.toHaveBeenCalledWith(
mockDomainObject,
mockSeries,
testIndex
);
});
});
}
);