diff --git a/docs/gendocs.js b/docs/gendocs.js index 51c84d9a64..10facc0ded 100644 --- a/docs/gendocs.js +++ b/docs/gendocs.js @@ -106,7 +106,7 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define } // Convert from Github-flavored Markdown to HTML - function gfmifier() { + function gfmifier(renderTOC) { var transform = new stream.Transform({ objectMode: true }), markdown = ""; transform._transform = function (chunk, encoding, done) { @@ -114,9 +114,11 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define done(); }; transform._flush = function (done) { - // Prepend table of contents - markdown = - [ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n"); + if (renderTOC){ + // Prepend table of contents + markdown = + [ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n"); + } this.push(header); this.push(marked(markdown)); this.push(footer); @@ -168,13 +170,16 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define var destination = file.replace(options['in'], options.out) .replace(/md$/, "html"), destPath = path.dirname(destination), - prefix = path.basename(destination).replace(/\.html$/, ""); + prefix = path.basename(destination).replace(/\.html$/, ""), + //Determine whether TOC should be rendered for this file based + //on regex provided as command line option + renderTOC = file.match(options['suppress-toc'] || "") === null; mkdirp(destPath, function (err) { fs.createReadStream(file, { encoding: 'utf8' }) .pipe(split()) .pipe(nomnomlifier(destPath, prefix)) - .pipe(gfmifier()) + .pipe(gfmifier(renderTOC)) .pipe(fs.createWriteStream(destination, { encoding: 'utf8' })); diff --git a/docs/src/design/index.md b/docs/src/design/index.md new file mode 100644 index 0000000000..7b4c3e4ebf --- /dev/null +++ b/docs/src/design/index.md @@ -0,0 +1,3 @@ +Design proposals: + +* [API Redesign](proposals/APIRedesign.md) \ No newline at end of file diff --git a/docs/src/design/planning/APIRefactor.md b/docs/src/design/planning/APIRefactor.md new file mode 100644 index 0000000000..8f693cb814 --- /dev/null +++ b/docs/src/design/planning/APIRefactor.md @@ -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]->[ Imperative bundle registration] + +[ Imperative bundle registration]->[ Build and packaging] +[ Imperative bundle registration]->[ Refactor API] + +[ Build and packaging | + [ Start]->[ Incorporate a build step] + [ Incorporate a build step | + [ Start]->[ Choose package manager] + [ Start]->[ Choose build system] + [ Choose build system]<->[ Choose package manager] + [ Choose package manager]->[ Implement] + [ Choose build system]->[ Implement] + [ Implement]->[ End] + ]->[ Separate repositories] + [ Separate repositories]->[ End] +]->[ Release candidacy] + + +[ Start]->[ Design registration API] + +[ Design registration API | + [ Start]->[ Decide on role of Angular] + [ Decide on role of Angular]->[ Design API] + [ Design API]->[ Passes review?] + [ Passes review?] no ->[ Design API] + [ Passes review?]-> yes [ End] +]->[ Refactor API] + +[ Refactor API | + [ Start]->[ Imperative extension registration] + [ Imperative extension registration]->[ Refactor individual extensions] + + [ Refactor individual extensions | + [ Start]->[ Prioritize] + [ Prioritize]->[ Sufficient value added?] + [ Sufficient value added?] no ->[ End] + [ Sufficient value added?] yes ->[ Design] + [ Design]->[ Passes review?] + [ Passes review?] no ->[ Design] + [ Passes review?]-> yes [ Implement] + [ Implement]->[ End] + ]->[ Remove legacy bundle support] + + [ Remove legacy bundle support]->[ End] +]->[ Release candidacy] + +[ Release candidacy | + [ Start]->[ Verify | + [ Start]->[ API well-documented?] + [ Start]->[ API well-tested?] + [ API well-documented?]-> no [ Write documentation] + [ API well-documented?] yes ->[ End] + [ Write documentation]->[ API well-documented?] + [ API well-tested?]-> no [ Write test cases] + [ API well-tested?]-> yes [ End] + [ Write test cases]->[ API well-tested?] + ] + [ Start]->[ Validate | + [ Start]->[ Passes review?] + [ Start]->[ Use internally] + [ Use internally]->[ Proves useful?] + [ Passes review?]-> no [ Address feedback] + [ Address feedback]->[ Passes review?] + [ Passes review?] yes -> [ End] + [ Proves useful?] yes -> [ End] + [ Proves useful?] no -> [ Fix problems] + [ Fix problems]->[ Use internally] + ] + [ Validate]->[ End] + [ Verify]->[ End] +]->[ Release] + +[ Release]->[ 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. \ No newline at end of file diff --git a/docs/src/design/proposals/APIRedesign.md b/docs/src/design/proposals/APIRedesign.md new file mode 100644 index 0000000000..479460b457 --- /dev/null +++ b/docs/src/design/proposals/APIRedesign.md @@ -0,0 +1,1282 @@ +# Overview + +The purpose of this document is to review feedback on Open MCT Web's +current API and propose improvements to the API, particularly for a +1.0.0 release. + +Strategically, this is handled by: + +* Identifying broader goals. +* Documenting feedback and related background information. +* Reviewing feedback to identify trends and useful features. + * In particular, pull out "pain points" to attempt to address, + as well as positive attributes to attempt to preserve. +* Proposing a set of API changes to address these "pain points." + * This also takes into account scheduling concerns. +* Once agreed-upon, formalize this set of changes (e.g. as UML + diagrams) and plan to implement them. + +# Goals + +## Characteristics of a good API + +A good API: + +* Is easy to understand. +* Rewards doing things "the right way." +* Saves development effort. +* Is powerful enough to support a broad range of applications. +* Lends itself to good documentation. + +These characteristics can sometimes be at odds with each other, or +with other concerns. These should typically be viewed as participants +in trades. + +## Evaluating APIs + +APIs may be evaluated based on: + +* Number of interfaces. + * How many application-specific interfaces do I need to know to + solve a certain class of problems? +* Size of interfaces. + * How many methods does each interface have? +* Depth of interfaces. + * Specifically, how many methods do I need to call before the return + value is of a form that is not specific to the API? +* Clarity of interfaces. + * How much documentation or learning is required before an interface is + useful? +* Consistency of interfaces. + * How similar is one interface to an analogous interface? +* Utility of interfaces. + * How much development effort is reduced by utilizing these interfaces, + versus accomplishing the same goals with other tools? +* Power of interfaces. + * How much application functionality can I influence with the interfaces + that are available to me? + +In general, prefer to have a small number of simple, shallow, clear, +useful, powerful interfaces. + +# Developer Feedback + +## Developer Intern Feedback + +This feedback comes from interns who worked closely with +Open MCT Web as their primary task over the Summer of 2015. + +### Developer Intern 1 + +Worked on bug fixes in the platform and a plugin for search. + +* Initially, it was confusing that many things in files that are in + very different locations in the code base refer to each other. + * Perhaps explain more the organization strategy behind the + different main sections, like "commonUI" vs "core". +* This may be just me, but there are often long chains of related + functions calling each other, and when I had to modify the behavior, + I had a hard time remembering to look for the highest level function + in the call chain to change. I also sometimes had a hard time finding + the connections between the functions. But, that is important because + the implementation of the functions along the chain may change later. +* One very helpful thing that you could add might just be documentation + that is not in paragraph format like in the current developer guide. + I would just like a list of all the functions and members of each kind + of object there is, and descriptions of what they are and how they're + used. + * Also, the current developer guide pdf's words that are in 'code font', + rather than the normal text, are not searchable. + (Depending on the pdf viewer.) +* I do appreciate that there is some example code. +* I am still slightly confused about what "domainObject" refers to in + different situations. +* The tutorials are helpful, but only really for designing new views. + It doesn't help much with gaining understanding of how the other parts + of the application work. +* The general idea of 'telemetry' in this context is kind of confusing. + It is hard to figure out what the difference between the various ways of + dealing with telemetry are. e.g., what is the difference between just + "Telemetry" and the "Telemetry Service"? There are many + "Telemetry Thing"s which seem related, but in an unclear way. + +### Developer Intern 2 + +Worked on platform bug fixes and mobile support. + +* No guide for the UI and front end for the HTML/CSS part of Open MCT Web. + Not sure if this is applicable or needed for developers, however would + be helpful to any front end development +* Found it difficult to follow the plot controller & subplot + functions/features, such as zooming. +* If the developer guide could have for references to which files or + functions are key for gestures, browse navigation, etc it would be + helpful for future developers as a place to start looking. I found + it occasionally difficult to find which files or functions I wanted + at first. + +## Plugin Developer Feedback + +This feedback comes from developers who have worked on plugins for +Open MCT Web, but have not worked on the platform. + +### Plugin Developer 1 + +Used Open MCT Web over the course of several months (on a +less-than-half-time basis) to develop a +spectrum visualization plugin. + +* Not a lot of time to work on this, made it hard to get up the learning + curve. + * Note that this is the norm, particularly for GDS development. +* JavaScript carries its own learning curve. +* The fact that it pulls in other tools whose APIs need to be learned + also makes the learning curve harder to get up. +* Tracking down interconnected parts was a bit difficult. +* Could really use examples. +* Easy to get lost when not immersed in the style. + +### Plugin Developer 2 + +Used Open MCT Web over the course of several weeks (on a half-time basis) +to develop a tabular visualization plugin. + +* Pain points + * Unable to copy and paste from tutorial pdfs into code + * Wanted to verify my environment was setup properly so that I + could get the final product working in the end without having + to type everything out. Perhaps there could be something in + github that has the final completed tutorial for new users to + checkout? Or a step by step one kind of like the tutorials on + the angular js webpage? + * Typing too long without seeing results of what I was doing + * At some points in the tutorial I ended up typing for the sake + of typing without knowing what I was really typing for. + * If there were break points where we could run the incomplete + code and just see a variable dump or something even that would + be helpful to know that I am on the right track. + * Documentation on features are a bit hard to find. + * I'm not sure what I can do until I search through examples of + existing code and work my way backwards. + * Maybe you can link the features we are using in the tutorial to + their respective parts in the developer guide? Not sure if that + can be done on PDFs, so maybe a webpage instead? +* Positive Attributes + * Unable to copy and paste from tutorial pdfs into code + * I know I also listed this as a pain, but it was kind of helpful + being forced to read and type everything out. + * "Widgets" are self contained in their own directories. I don't have + to be afraid of exploding things. + * All files/config that I care about for a "widget" can be found in + the bundles.json +* Misc + * Coming from a not so strong webdev background and on top of that a + zero strong angular background I think starting off with a simple + "Hello World" webpage tutorial would have been nice. + * Start off with a bare bones bundle json with an empty controller + and static "Hello World" in the view + * Add the variable "Hello World" into the controller for the view + to display + * Add a model property to the bundle.json to take in "Hello World" + as a parameter and pass through to the controller/view + +### Open Source Contributer + + * [Failures are non-graceful when services are missing.]( + https://github.com/nasa/openmctweb/issues/79) + +## Misc. Feedback (mostly verbal) + +* Easy to add things. +* Separation of concerns is unclear (particularly: "where's the MVC?") +* Telemetry API is confusing. In particular, `TelemetrySeries` should + just be an array. + * Came out of design discussions for Limits. +* Capabilities are confusing. + +## Long-term Developer Notes + +The following notes are from original platform developer, with long +term experience using Open MCT Web. + +* Bundle mechanism allows for grouping related components across concerns, + and adding and removing these easily. (e.g. model and view components of + Edit mode are all grouped together in the Edit bundle.) + +## AngularJS + +Angular 2.0.0 is coming (maybe by end of 2015.) + +It will not be backwards-compatible with Angular 1.x. +The differences are significant enough that switching to +Angular 2 will require only slightly less effort than switching +to an entirely different framework. + +We can expect AngularJS 1.x to reach end-of-life reasonably soon thereafter. + +Our API is currently a superset of Angular's API, so this directly effects +our API. Specifically, API changes should be oriented towards removing +or reducing the Angular dependency. + +### Angular's Role + +Angular is Open MCT Web's: + +* Dependency injection framework. +* Template rendering. +* DOM interactions. +* Services library. +* Form validator. +* Routing. + +This is the problem with frameworks: They become a single point of +failure for unrelated concerns. + +### Rationale for Adopting Angular + +The rationale for adopting AngularJS as a framework is +documented in https://trunk.arc.nasa.gov/jira/browse/WTD-208. +Summary of the expected benefits: + +* Establishes design patterns that are well-documented and + understood in industry. This can be beneficial in training + new staff, and lowers the documentation burden on the local + development team. If MCT-Web were to stay with its current + architecture, significant developer-oriented documentation + and training materials would need to be produced. +* The maintainability of MCT-Web would be enhanced by using a + framework like Angular. The local team would enjoy the benefits of + maintenance performed by the sponsor, but would not incur any cost + for this. This would include future upgrades, testing, and bug fixes. +* Replaces DOM-manipulation with a declarative data-binding syntax + which automatically updates views when the model data changes. This + pattern has the potential to save the development team from + time-consuming and difficult-to-debug DOM manipulation. +* Provides data binding to backend models. +* Provides patterns for form validation. +* Establishes documented patterns for add-on modules and services. +* Supports unit tests and system tests (tests which simulate user + interactions in the browser) +* Angular software releases can be expected to be tested, which would + allow MCT-Web developers to focus on MCT-specific features, instead + of the maintenance of custom infrastructure. + +### Actual Experience with Angular + +Most of the expected benefits of Angular have been invalidated +by experience: + +* Feedback from new developers is that Angular was a hindrance to + training, not a benefit. ("One more thing to learn.") Significant + documentation remains necessary for Open MCT Web. +* Expected enhancements to maintainability will be effectively + invalidated by an expected Angular end-of-life. +* Data binding and automatic view updates do save development effort, + but also carry a performance penalty. This can be solved, but requires + resorting to exactly the sort of DOM manipulations we want to avoid. + In some cases this can require more total development (writing a + poorly-performing Angular version, then "optimizing" by rewriting a + non-Angular version.) +* Expected reduction of test scope will also be invalidated by an + expected end-of-life. + +Other problems: + +* Hinders integrating non-Angular components. (Need to wrap with + Angular API, e.g. as directives, which may be non-trivial.) +* Interferes with debugging by swallowing or obscuring exceptions. + +# Feedback Review + +## Problem Summary + +The following attributes of the current API are undesirable: + +- [ ] It is difficult to tell "where things are" in the code base. +- [ ] It is difficult to see how objects are passed around at run-time. +- [ ] It is difficult to trace flow of control generally. +- [ ] Multiple interfaces for related concepts (e.g. telemetry) is confusing. +- [ ] API documentation is missing or not well-formatted for use. +- [ ] High-level separation of concerns is not made clear. +- [ ] Interface depth of telemetry API is excessive (esp. `TelemetrySeries`) +- [ ] Capabilities as a concept lack clarity. +- [ ] Too many interfaces and concepts to learn. +- [ ] Exposing third-party APIs (e.g. Angular's) increases the learning curve. +- [ ] Want more examples, easier-to-use documentation. +- [ ] UI-relevant features (HTML, CSS) under-documented +- [ ] Good MVC for views of domain objects not enforced (e.g. plots) + +## Positive Features + +It is desirable to retain the following features in an API redesign: + +- [ ] Creating new features and implementing them additively is well-supported. +- [ ] Easy to add/remove features which involve multiple concerns. +- [ ] Features can be self-contained. +- [ ] Declarative syntax makes it easy to tell what's in use. + +## Requirements + +The following are considered "must-haves" of any complete API +redesign: + +- [ ] Don't require usage of Angular API. +- [ ] Don't require support for Angular API. + +# Proposals + +## RequireJS as dependency injector + +Use Require.JS for dependency injection. + +Dependencies will then be responsible for being sufficiently +mutable/extensible/customizable. This can be facilitated by +adding platform classes which can facilitate the addition +of reusable components. + +Things that we usefully acquire via dependency injection currently: + +* Services. +* Extensions (by category). +* Configuration constants. + +Services would be defined (by whatever component is responsible +for declaring it) using `define` and the explicit name of the +service. To allow for the power of composite services, the +platform would provide a `CompositeService` class that supports +this process by providing `register`, `decorate`, and `composite` +methods to register providers, decorators, and aggregators +respectively. (Note that nomenclature changes are also implied +here, to map more clearly to the Composite Pattern and to +avoid the use of the word "provider", which has ambiguity with +Angular.) + +```js +define( + "typeService", + ["CompositeService"], + function (CompositeService) { + var typeService = new CompositeService([ + "listTypes", + "getType" + ]); + + // typeService has `listTypes` and `getType` as methods; + // at this point they are stubbed (will return undefined + // or throw or similar) but this will change as + // decorators/compositors/providers are added. + + // You could build in a compositor here, or + // someone could also define one later + typeService.composite(function (typeServices) { + // ... return a TypeService + }); + + // Similarly, you could register a default implementation + // here, or from some other script. + typeService.register(function (typeService) { + // ... return a TypeService + }, { priority: 'default' }); + + return typeService; + } +); +``` + +Other code could then register additional `TypeService` +implementations (or decorators, or even compositors) by +requiring `typeService` and calling those methods; or, it +could use `typeService` directly. Priority ordering could +be utilized by adding a second "options" argument. + +For extension categories, you could simply use registries: + +```js +define( + "typeRegistry", + ["ExtensionRegistry"], + function (ExtensionRegistry) { + return new ExtensionRegistry(); + } +); +``` + +Where `ExtensionRegistry` extends `Array`, and adds a +`register` method which inserts into the array at some +appropriate point (e.g. with an options parameter that +respects priority order.) + +This makes unit testing somewhat more difficult when you +want to mock injected dependencies; there are tools out +there (e.g. [Squire](https://github.com/iammerrick/Squire.js/)) +which can help with this, however. + +### Benefits + +* Clarifies "how objects are passed around at run-time"; + answer is always "via RequireJS." +* Preserves flexibility/power provided by composite services. +* Lends itself fairly naturally to API documentation via JSDoc + (as compared to declaring things in bundles, which does not.) +* Reduces interface complexity for acquiring dependencies; + one interface for both explicit and "implicit" dependencies, + instead of separate approaches for static and substitutable + dependencies. +* Removes need to understand Angular's DI mechanism. +* Improves useability of documentation (`typeService` is an + instance of `CompositeService` and implements `TypeService` + so you can easily traverse links in the JSDoc.) +* Can be used more easily from Web Workers, allowing services + to be used on background threads trivially. + +### Detriments + +* Having services which both implement the service, and + have methods for registering the service, is a little + weird; would be cleaner if these were separate. + (Mixes concerns.) +* Syntax becomes non-declarative, which may make it harder to + understand "what uses what." +* Allows for ordering problems (e.g. you start using a + service before everything has been registered.) + +## Arbitrary HTML Views + +Currently, writing new views requires writing Angular templates. +This must change if we want to reduce our dependence on Angular. + +Instead, propose that: + +* What are currently called "views" we call something different. + (Want the term view to be more like "view" in the MVC sense.) + * For example, call them "applications." +* Consolidate what are currently called "representations" and + "templates", and instead have them be "views". + +For parity with actions, a `View` would be a constructor which +takes an `ActionContext` as a parameter (with similarly-defined +properties) and exposes a method to retrieve the HTML elements +associateed with it. + +The platform would then additionally expose an `AngularView` +implementation to improve compatibility with existing +representations, whose usage would something like: + +```js +define( + ["AngularView"], + function (AngularView) { + var template = "Hello world"; + return new AngularView(template); + } +); +``` + +The interface exposed by a view is TBD, but should provide at +least the following: + +* A way to get the HTML elements that are exposed by & managed + by the view. +* A `destroy` method to detach any listeners at the model level. + +Individual views are responsible for managing their resources, +e.g. listening to domain objects for mutation. To keep DRY, the +platform should include one or more view implementations that +can be used/subclassed which handle common behavior(s). + +### Benefits + +* Using Angular API for views is no longer required. +* Views become less-coupled to domain objects. Domain objects + may be present in the `ViewContext`, but this also might + just be a "view" of some totally different thing. +* Helps clarify high-level concerns in the API (a View is now + really more like a View in the MVC sense; although, not + completely, so this gets double-booked as a detriment.) +* Having a `ViewContext` that gets passed in allows views to + be more "contextually aware," which is something that has + been flagged previously as a UX desire. + +### Detriments + +* Becomes less clear how views relate to domain objects. +* Adds another interface. +* Leaves an open problem of how to distinguish views that + a user can choose (Plot, Scrolling List) from views that + are used more internally by the application (tree view.) +* Views are still not Views in the MVC sense (in practice, + the will likely be view-controller pairs.) We could call + them widgets to disambiguate this. +* Related to the above, even if we called these "widgets" + it would still fail to enforce good MVC. + +## Wrap Angular Services + +Wrap Angular's services in a custom interfaces; e.g. +replace `$http` with an `httpService` which exposes a useful +subset of `$http`'s functionality. + +### Benefits + +* Removes a ubiquitous dependency on Angular. +* Allows documentation for these features to be co-located + and consistent with other documentation. +* Facilitates replacing these with non-Angular versions + in the future. + +### Detriments + +* Increases the number of interfaces in Open MCT Web. (Arguably, + not really, since the same interfaces would exist if exposed + by Angular.) + +## Bundle Declarations in JavaScript + +Replace `bundle.json` files (and bundle syntax generally) with +an imperative form. There would instead be a `Bundle` interface +which scripts can implement (perhaps assisted by a platform +class.) + +The `bundles.json` file would then be replaced with a `bundles.js` +or `Bundles.js` that would look something like: + +```js +define( + [ + 'platform/core/PlatformBundle', + // ... etc ... + 'platform/features/plot/PlotBundle' + ], + function () { + return arguments; + } +); +``` + +Which could in turn be used by an initializer: + +```js +define( + ['./bundles', 'mct'], + function (bundles, mct) { + mct.initialize(bundles); + } +); +``` + +A `Bundle` would have a constructor that took some JSON object +(a `BundleContext`, lets say) and would provide methods for +application life-cycle events. Depending on other choices, +a dependency injector could be passed in at some appropriate +life-cycle call (e.g. initialize.) + +This would also allow for "composite bundles" which serve as +proxies for multiple bundles. The `BundleContext` could contain +(or later be amended to contain) filtering rules to ignore +other bundles and so forth (this has been useful for administering +Open MCT Web in subtly different configurations in the past.) + +### Benefits + +* Imperative; more explicit, less magic, more clear what is going on. +* Having a hierarchy of "bundles" could make it easier to navigate + (relevant groupings can be nested in a manner which is not + currently well-supported.) +* Lends itself naturally to a compilation step. +* Nudges plugin authors to "do your initialization and registration + in a specific place" instead of mixing in registration of features + with their implementations. + +### Detriments + +* Introduces another interface. +* Loses some of the convenience of having a declarative + summary of components and their dependencies. + +## Pass around a dependency injector + +:warning: Note that this is incompatible with the +[RequireJS as dependency injector](#requirejs-as-dependency-injector) +proposal. + +Via some means (such as in a registration lifecycle event as +described above) pass a dependency injector to plugins to allow +for dependencies to be registered. + +For example: + +```js +MyBundle.prototype.registration = function (architecture) { + architecture.service('typeService').register(MyTypeService); + architecture.extension('actions').register( + [ 'foo' ], + function (foo) { return new MyAction(foo); } + ); +}; +``` + +### Benefits + +* Ensures that registration occurs at an appropriate stage of + application execution, avoiding start-up problems. +* Makes registration explicit (generally easier to understand) + rather than implicit. +* Encapsulates dependency injection nicely. + +### Detriments + +* Increases number of interfaces to learn. +* Syntax likely to be awkward, since in many cases we really + want to be registering constructors. + +## Remove partial constructors + +Remove partial constructors; these are confusing. It is hard to +recognize which constructor arguments are from dependencies, and +which will be provided at run-time. Instead, it is the responsibility +of whoever is introducing a component to manage these things +separately. + +### Benefits + +* More clarity. + +### Detriments + +* Possibly results in redundant effort to manage this difference + (other APIs may need to be adjusted accordingly.) + +## Rename Views to Applications + +Rename (internally to the application, not necessarily in UI or +in user guide) what are currently called `views` to `applications`. + +### Benefits + +* Easier to understand. What is currently called a "view" is, + in the MVC sense, a view-controller pair, usually with its own + internal model for view state. Calling these "applications" + would avoid this ambiguity/inconsistency. +* Also provides an appropriate mindset for building these; + particularly, sets the expectation that you'll want to decompose + this "application" into smaller pieces. This nudges developers + in appropriate directions (in contrast to `views`, which + typically get implemented as templates with over-complicated + "controllers".) + +### Detriments + +* Developer terminology falls out of sync with what is used in + the user guide. + +## Provide Classes for Extensions + +As a general pattern, when introducing extension categories, provide +classes with a standard implementation of these interfaces that +plugin developers can `new` and register. + +For example, instead of declaring a type as: + +```json +{ + "types": [{ + "key": "sometype", + "glyph": "X", + "etc": "..." + }] +} +``` + +You would register one as: + +```js +// Assume we have gotten a reference to a type registry somehow +typeRegistry.register(new Type({ + "key": "sometype", + "glyph": "X", + "etc": "..." +})); +``` + +### Benefits + +* Easier to understand (less "magic"). +* Lends itself naturally to substitution of different implementations + of the same interface. +* Allows for run-time decisions about exactly what gets registered. + +### Detriments + +* Adds some modest boilerplate. +* Provides more opportunity to "do it wrong." + +## Normalize naming conventions + +Adopt and obey the following naming conventions for AMD modules +(and for injectable dependencies, which may end up being modules): + +* Use `UpperCamelCase` for classes. +* Use `lowerCase` names for instances. + * Use `someNoun` for object instances which implement some + interface. The noun should match the implemented interface, + when applicable. + * `useSomeVerb` for functions. +* Use `ALL_CAPS_WITH_UNDERSCORES` for other values, including + "struct-like" objects (that is, where the object conceptually + contains properties rather than methods.) + +### Benefits + +* Once familiar with the conventions, easier to understand what + individual modules are. + +### Detriments + +* A little bit inflexible. + +## Expose no third-party APIs + +As a general practice, expose no third-party APIs as part of the +platform. + +For cases where you do want to access third-party APIs directly +from other scripts, this behavior should be "opt-in" instead of +mandatory. For instance, to allow addition of Angular templates, +an Angular-support bundle could be included which provides an +`AngularView` class, a `controllerRegistry`, et cetera. Importantly, +such a bundle would need to be kept separate from the platform API, +or appropriately marked as non-platform in the API docs (an +`@experimental` tag would be nice here if we feel like extending +JSDoc.) + +### Benefits + +* Simplifies learning curve (only one API to learn.) +* Reduces Angular dependency. +* Avoids the problems of ubiquitous dependencies generally. + +### Detriments + +* Increases documentation burden. + +## Register Extensions as Instances instead of Constructors + +Register extensions as object instances instead of constructors. +This allows for API flexibility w.r.t. constructor signatures +(and avoids the need for partial constructors) and additionally +makes it easier to provide platform implementations of extensions +that can be used, subclassed, etc. + +For instance, instead of taking an `ActionContext` in its +constructor, an `Action` would be instantiated once and would +accept appropriate arguments to its methods: + +```js +function SomeAction { +} +SomeAction.prototype.canHandle = function (actionContext) { + // Check if we can handle this context +}; +SomeAction.prototype.perform = function (actionContext) { + // Perform this action, in this context +}; +``` + +### Benefits + +* Reduces scope of interfaces to understand (don't need to know + what constructor signature to provide for compatibility.) + +### Detriments + +* Requires refactoring of various types; may result in some + awkward APIs or extra factory interfaces. + +## Remove capability delegation + +The `delegation` capability has only been useful for the +`telemetry` capability, but using both together creates +some complexity to manage. In practice, these means that +telemetry views need to go through `telemetryHandler` to +get their telemetry, which in turn has an awkward API. + +This could be resolved by: + +* Removing `delegation` as a capability altogether. +* Reworking `telemetry` capability API to account for + the possibility of multiple telemetry-providing + domain objects. (Perhaps just stick `domainObject` + in as a field in each property of `TelemetryMetadata`?) +* Move the behavior currently found in `telemetryHandler` + into the `telemetry` capability itself (either the + generic version, or a version specific to telemetry + panels - probably want some distinct functionality + for each.) + +### Benefits + +* Reduces number of interfaces. +* Accounting for the possibility of multiple telemetry objects + in the `telemetry` capability API means that views using + this will be more immediately aware of this as a possibility. + +### Detriments + +* Increases complexity of `telemetry` capability's interface + (although this could probably be minimized.) + +## Nomenclature Change + +Instead of presenting Open MCT Web as a "framework" or +"platform", present it as an "extensible application." + +This is mostly a change for the developer guide. A +"framework" and a "platform" layer would still be useful +architecturally, but the plugin developer's mental model +for this would then be inclined toward looking at defined +extension points. The underlying extension mechanism could +still be exposed to retain the overall expressive power of +the application. + +This may subtly influence other design decisions in order +to match the "extensible application" identity. On a certain +level, this contradicts the proposal to +[rename views to applications](#rename-views-to-applications). + +### Benefits + +* May avoid incurring some of the "framework aversion" that + is common among JavaScript developers. +* More accurately describes the application. + +### Detriments + +* May also be a deterrent to developers who prefer the more + "green field" feel of developing applications on a useful + platform. + +## Capabilities as Mixins + +Change the behavior of capabilities such that they act as +mixins, adding additional methods to domain objects. +Checking if a domain object has a `persistence` capability +would instead be reduced to checking if it has a `persist` +method. + +Mixins would be applied in priority order and filtered for +applicability by policy. + +### Benefits + +* Replaces "capabilities" (which, as a concept, can be hard + to grasp) with a more familiar "mixins" concept, which has + been used more generally across many languages. +* Reduces interface depth. + +### Detriments + +* Requires checking for the interface exposed by a domain + object. Alternately, could use `instanceof`, but would + need to take care to ensure that the prototype chain of + the domain object is sufficient to do this (which may + enforce awkward or non-obvious constraints on the way these + mixins are implemented.) +* May complicate documentation; understanding the interface + of a given domain object requires visiting documentation + for various mixins. + +## Remove Applies-To Methods + +Remove all `appliesTo` static methods and replace them with +appropriate policy categories. + +### Benefits + +* Reduces sizes of interfaces. Handles filtering down sets + of extensions in a single consistent way. + +### Detriments + +* Mixes formal applicability with policy; presently, `appliesTo` + is useful for cases where a given extension cannot, even in + principle, be applied in a given context (e.g. a domain object + model is missing the properties which describe the behavior), + whereas policy is useful for cases where applicability is + being refined for business or usability reasons. Colocating + the former with the extension itself has some benefits + (exhibits better cohesion.) + * This could be mitigated in the proposed approach by locating + `appliesTo`-like policies in the same bundle as the relevant + extension. + +## Revise Telemetry API + +Revise telemetry API such that: + +* `TelemetrySeries` is replaced with arrays of JavaScript objects + with properties. +* It is no longer necessary to use `telemetryHandler` (plays well + with proposal to + [remove capability delegation](#remove-capability delegation)) +* Change `request` call to take a callback, instead of returning + a promise. This allows that callback to be invoked several + times (e.g. for progressive loading, or to reflect changes from + the time conductor.) + +Should also consider: + +* Merge `subscribe` functionality into `request`; that is, handle + real-time data as just another thing that triggers the `request` + callback. +* Add a useful API to telemetry metadata, allowing things like + formats to be retrieved directly from there. + +As a consequence of this, `request` would need to return an object +representing the active request. This would need to be able to +answer the following questions and provide the following behavior: + +* Has the request been fully filled? (For cases like progressive + loading?) +* What data has changed since the previous callback? (To support + performance optimizations in plotting; e.g. append real-time + data.) +* Stop receiving updates for this request. +* Potentially, provide utility methods for dealing with incoming + data from the request. + +Corollary to this, some revision of `TelemetryMetadata` properties +may be necessary to fully and usably describe the contents of +a telemetry series. + +### Benefits + +* Reduces interface depth. +* Reduces interface size (number of methods.) +* Supports a broader range of behaviors (e.g. progressive loading) + within the same interface. + +### Detriments + +* Merging with `subscribe` may lose the clarity/simplicity of the + current API. + +## Allow Composite Services to Fail Gracefully + +Currently, when no providers are available for a composite service +that is depended-upon, dependencies cannot be resolved and the +application fails to initialize, with errors appearing in the +developer console. + +This is acceptable behavior for truly unrecoverable missing +dependencies, but in many cases it would be preferable to allow a +given type of composite service to define some failure behavior +when no service of an appropriate type is available. + +To address this: + +* Provide an interface (preferably + [imperative](#bundle-Declarations-in-javascript)) + for declaring composite services, independent of any implementation + of an aggregator/decorator/provider. This allows the framework + layer to distinguish between unimplemented dependencies (which + could have defined failover strategies) from undefined dependencies + (which cannot.) +* Provide a default strategy for service composition that picks + the highest-priority provider, and logs an error (and fails to + satisfy) if no providers have been defined. +* Allow this aggregation strategy to be overridden, much as one + can declare aggregators currently. However, these aggregators should + get empty arrays when no providers have been registered (instead of + being ignored), at which point they can decide how to handle this + situation (graceful failure when it's possible, noisy errors when + it is not.) + +### Benefits + +* Allows for improved robustness and fault tolerance. +* Makes service declarations explicit, reducing "magic." + +### Detriments + +* Requires the inclusion of software units which define services, + instead of inferring their existence (slight increase in amount + of code that needs to be written.) +* May result in harder-to-understand errors when overridden + composition strategies do not failover well (that is, when they + do need at least implementation, but fail to check for this.) + +## Plugins as Angular Modules + +Do away with the notion of bundles entirely; use Angular modules +instead. Registering extensions or components of composite services +would then be handled by configuring a provider; reusable classes +could be exposed by the platform for these. + +Example (details are flexible, included for illustrative purposes): + +```javascript +var mctEdit = angular.module('mct-edit', ['ng', 'mct']); + +// Expose a new extension category +mctEdit.provider('actionRegistry', ExtensionCategoryProvider); + +// Expose a new extension +mctEdit.config(['actionRegistryProvider', function (arp) { + arp.register(EditPropertiesAction); +}]) + +return mctEdit; +``` + +Incompatible with proposal to +(expose no third-party APIs)[#expose-no-third-party-apis]; Angular +API would be ubiquitously exposed. + +This is a more specific variant of +(Bundle Declarations in JavaScript)[#bundle-declarations-in-javascript]. + +### Benefits + +* Removes a whole category of API (bundle definitions), reducing + learning curve associated with the software. +* Closer to Angular style, reducing disconnect between learning + Angular and learning Open MCT Web (reducing burden of having + to learn multiple paradigms.) +* Clarifies "what can be found where" (albeit not perfectly) + since you can look to module dependencies and follow back from there. + +### Detriments + +* Hardens dependency on Angular. +* Increases depth of understanding required of Angular. +* Increases amount of boilerplate (since a lot of this has + been short-handed by existing framework layer.) + +## Contextual Injection + +For extensions that depend on certain instance-level run-time +properties (e.g. actions or views which use objects and/or specific +capabilities of those objects), declare these features as dependencies +and expose them via dependency injection. (AngularJS does this for +`$scope` in the context of controllers, for example.) + +A sketch of an implementation for this might look like: + +```js +function ExtensionRegistry($injector, extensions, getLocals) { + this.$injector = $injector; + this.extensions = extensions; + this.getLocals = getLocals; +} +ExtensionRegistry.prototype.get = function () { + var $injector = this.$injector, + locals = this.getLocals.apply(null, arguments); + return this.extensions.filter(function (extension) { + return depsSatisfiable(extension, $injector, locals); + }).map(function (extension) { + return $injector.instantiate(extension, locals); + }); +}; + + +function ExtensionRegistryProvider(getLocals) { + this.getLocals = getLocals || function () { return {}; }; + this.extensions = []; +} +ExtensionRegistryProvider.prototype.register = function (extension) { + this.extensions.push(extension); +}; +ExtensionRegistryProvider.prototype.$get = ['$injector', function ($injector) { + return new ExtensionRegistry($injector, this.extensions, this.getLocals); +}]; +``` + +Extension registries which need to behave context-sensitively could +subclass this to describe how these contextual dependencies are satisfied +(for instance, by returning various capability properties in `getLocals`). + +Specific extensions could then declare dependencies as appropriate to the +registry they are using: + +```js +app.config(['actionRegistryProvider', function (arp) { + arp.register(['contextCapability', 'domainObject', RemoveAction]); +}]); +``` + +### Benefits + +* Allows contextual dependencies to be fulfilled in the same (or similar) + manner as global dependencies, increasing overall consistency of API. +* Clarifies dependencies of individual extensions (currently, extensions + themselves or policies generally need to imperatively describe what + dependencies will be used in order to filter down to applicable + extensions.) +* Factors out some redundant code from relevant extensions; actions, + for instance, no longer need to interpret an `ActionContext` object. + Instead, their constructors take inputs that are more relevant to + their behavior. +* Removes need for partial construction, as any arguments which would + normally be appended after partialization can instead be declared as + dependencies. Constructors in general become much less bound to the + specifics of the platform. + +### Detriments + +* Slightly increases dependency on Angular; other dependency injectors + may not offer comparable ways to specificy dependencies non-globally. +* Not clear (or will take effort to make clear) which dependencies are + available for which extensions. Could be mitigated by standardizing + descriptions of context across actions and views, but that may offer + its own difficulties. +* May seem counter-intuitive coming from "vanilla" AngularJS, where + `$scope` is the only commonly-used context-sensitive dependency. + +## Add new abstractions for actions + +Originally suggested in +[this comment](https://github.com/nasa/openmctweb/pull/69#issuecomment-156199991): + +> I think there are some grey areas with actions: are they all directly +tied to user input? If so, why do they have any meaning in the back end? +Maybe we should look at different abstractions for actions: + +> * `actions` - the basic implementation of an action, essentially a + function declaration. for example, `copy` requires arguments of + `object` and a `target` to place the object in. at this level, + it is reusable in a CLI. +> * `context menu actions` - has criteria for what it applies to. + when it is visible, and defines how to get extra > input from a + user to complete that action. UI concern only. +> * `gesture-handler` - allows for mapping a `gesture` to an action, + e.g. drag and drop for link. UI Concern only. + +> We could add context menu actions for domain objects which navigate +to that object, without having to implement an action that has no real +usage on a command line / backend. + +### Benefits + +* Clearly delineates concerns (UI versus model) + +### Detriments + +* Increases number of interfaces. + +## Add gesture handlers + +See [Add new abstractions for actions](#add-new-abstractions-for-actions); +adding an intermediary between gestures and the actions that they +trigger could be useful in separating concerns, and for more easily +changing mappings in a mobile context. + +### Benefits + +* Clearly decouples UI concerns from the underlying model changes + they initiate. +* Simplifies and clarifies mobile support. + +### Detriments + +* Increases number of interfaces. + +# Decisions + +After review on Dec. 8, 2015, team consensus on these proposals is +as follows: + +Proposal | @VWoeltjen | @larkin | @akhenry | Consensus +----|:---:|:---:|:---:|:---: +RequireJS as dependency injector | :-1: | :neutral_face: :question: | [:-1:](https://github.com/nasa/openmctweb/pull/69#discussion_r44349731) | [:question:](https://github.com/nasa/openmctweb/issues/461) +Arbitrary HTML Views | :+1: | :+1: | | [:+1: 1](https://github.com/nasa/openmctweb/issues/463) +Wrap Angular Services | :-1: | [:-1:](https://github.com/nasa/openmctweb/pull/69#discussion_r43801221) | [:-1:](https://github.com/nasa/openmctweb/pull/69#discussion_r44355057) | :no_entry_sign: +Bundle Declarations in JavaScript | :+1: | :neutral_face: :question: | | [:+1:](https://github.com/nasa/openmctweb/issues/450) +Pass around a dependency injector | :-1: | :-1: | | :-1: +Remove partial constructors | :+1: | :+1: | | [:+1:](https://github.com/nasa/openmctweb/issues/462) +Rename Views to ~~Applications~~ | :+1: | :neutral_face: :question: | | [:+1: 2](https://github.com/nasa/openmctweb/issues/463) +Provide Classes for Extensions | :+1: | :+1: | | [:+1:](https://github.com/nasa/openmctweb/issues/462) +Normalize naming conventions | :+1: | :+1: | | :+1: +Expose no third-party APIs | :+1: * | [:-1:](https://github.com/nasa/openmctweb/pull/69#discussion_r43801221) | [:+1:](https://github.com/nasa/openmctweb/pull/69#discussion_r43801221) † | :+1: 3 +Register Extensions as Instances instead of Constructors | :+1: | :-1: | | [:+1:](https://github.com/nasa/openmctweb/issues/462) +Remove capability delegation | :+1: | :+1: | | [:+1:](https://github.com/nasa/openmctweb/issues/463) +Nomenclature Change | :+1: | [:+1:](https://github.com/nasa/openmctweb/issues/229#issuecomment-153453035) | | :white_check_mark: ‡ +Capabilities as Mixins | | :+1: | [:+1:](https://github.com/nasa/openmctweb/pull/69#discussion_r44355473) | [:question: 4](https://github.com/nasa/openmctweb/issues/463) +Remove Applies-To Methods | | :-1: | | :-1: +Revise Telemetry API | :+1: | :+1: | | [:+1: 5](https://github.com/nasa/openmctweb/issues/463) +Allow Composite Services to Fail Gracefully | :+1: | :-1: | | [:+1: 6](https://github.com/nasa/openmctweb/issues/463) +Plugins as Angular Modules | :+1: | :neutral_face: :question: | | [:question:](https://github.com/nasa/openmctweb/issues/461) +Contextual Injection | | :-1: | | [:question:](https://github.com/nasa/openmctweb/issues/461) +Add new abstractions for actions | [:-1:](https://github.com/nasa/openmctweb/pull/69#issuecomment-158172485) :question: | :+1: | | :-1: +Add gesture handlers | :+1: | :+1: :question: | | [:+1:](https://github.com/nasa/openmctweb/issues/463) + +* Excepting Angular APIs. Internally, continue to use code style +where classes are declared separately from their registration, such +that ubiquity of Angular dependency is minimized. + +† "I think we should limit the third party APIs we expose to +one or two, but I worry it might be counterproductive to +completely hide them." + +‡ Some ambiguity about what to call ourselves if not a platform, +but general agreement that "platform" is not a good term. +More Detail on Pete's Opinions Here: +https://github.com/nasa/openmctweb/blob/api-redesign/docs/src/design/proposals/APIRedesign_PeteRichards.md#notes-on-current-api-proposals + +1 Needs to be designed carefully; don't want to do this with +a complicated interface, needs to be significantly simpler than wrapping +with an Angular directive would be. + +2 Agree that we need a new name, but it should not be "application" + +3 Don't want to expose (or require usage of) third-party +APIs generally. Angular may be an exception in the sense that it is an +API we presume to be present. Can use third-party APIs internally, but +don't want to support them or be tied to them. + +4 Want to have a separate spin-off discussion about +capabilities. Want to consider several alternatives here. +At minimum, though, mixins would be an improvement relative +to how these are currently handled. + +5 Agree we want to revise APIs, but this should +be a larger spin-off. + +6 Not necessarily as described, but expected to be a +property of composite services in whatever formulation they +take. Should not be default behavior. + + +[Additional proposals](APIRedesign_PeteRichards.md) considered: + +Proposal | Consensus +------|------ +Imperitive component registries | [:+1:](https://github.com/nasa/openmctweb/issues/462) +Get rid of "extension category" concept. | [:+1:](https://github.com/nasa/openmctweb/issues/462) +Reduce number and depth of extension points | :+1: +Composite services should not be the default | [:question:](https://github.com/nasa/openmctweb/issues/463) +Get rid of views, representations, and templates. | [:+1: 1](https://github.com/nasa/openmctweb/issues/463) +More angular: for all services | [:question:](https://github.com/nasa/openmctweb/issues/461) +Less angular: only for views | [:question:](https://github.com/nasa/openmctweb/issues/461) +Use systemjs for module loading | [:+1: 2](https://github.com/nasa/openmctweb/issues/459) +Use gulp or grunt for standard tooling | [:+1:](https://github.com/nasa/openmctweb/issues/459) +Package openmctweb as single versioned file. | [:+1:](https://github.com/nasa/openmctweb/issues/458) +Refresh on navigation | [:+1: 3](https://github.com/nasa/openmctweb/issues/463) +Move persistence adapter to promise rejection. | [:+1:](https://github.com/nasa/openmctweb/issues/463) +Remove bulk requests from providers | [:+1: 4](https://github.com/nasa/openmctweb/issues/463) + +1 Need to agree upon details at design-time, but +basic premise is agreed-upon - want to replace +views/representations/templates with a common abstraction +(and hoist out the non-commonalities to other places as appropriate) + +2 Beneficial but not strictly necessary (may be +lower-effort alternatives); should prioritize accordingly during planning + +3 Some effort will be required to make all of the state +that needs to persist among route changes actually be persistent. +Will want to address this at design-time (will want to look at +libraries to simplify this, for instance) + +4 Maybe not all providers, but anywhere there is not a +strong case for building batching into the API we should prefer +simplicity. (Want to pay specific attention to telemetry here.) diff --git a/docs/src/design/proposals/APIRedesign_PeteRichards.md b/docs/src/design/proposals/APIRedesign_PeteRichards.md new file mode 100644 index 0000000000..3fa78fe2ed --- /dev/null +++ b/docs/src/design/proposals/APIRedesign_PeteRichards.md @@ -0,0 +1,251 @@ + + +**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) + + + + +# 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 + + +``` + +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. + diff --git a/docs/src/design/proposals/ImperativePlugins.md b/docs/src/design/proposals/ImperativePlugins.md new file mode 100644 index 0000000000..1fbb83b1bc --- /dev/null +++ b/docs/src/design/proposals/ImperativePlugins.md @@ -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. + | + - factoryFn : function (V) : T + | + + decorate(decoratorFn : function (T, V) : T, options? : RegistrationOptions) +]-:>[function (V) : T] + +[RegistrationOptions | + + priority : number or string +] + +[Registry. + | + - compositorFn : function (Array.) : V + | + + register(item : T, options? : RegistrationOptions) + + composite(compositorFn : function (Array.) : V, options? : RegistrationOptions) +]-:>[Factory.] +[Factory.]-:>[Factory.] + +[ExtensionRegistry.]-:>[Registry.>] +[Registry.>]-:>[Registry.] + +[CompositeServiceFactory.]-:>[Registry.] +[Registry.]-:>[Registry.] +``` + +## 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. \ No newline at end of file diff --git a/docs/src/design/proposals/Roles.md b/docs/src/design/proposals/Roles.md new file mode 100644 index 0000000000..6148d47566 --- /dev/null +++ b/docs/src/design/proposals/Roles.md @@ -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. + | + - 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. | + + validate(domainObject : DomainObject) : boolean + + decorate(decoratorFn : function (T, V) : T, options? : RoleOptions) +]-:>[Factory.] +[Factory.]-:>[Factory.] +``` + +## 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. \ No newline at end of file diff --git a/docs/src/index.html b/docs/src/index.html deleted file mode 100644 index 727523eda3..0000000000 --- a/docs/src/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - Open MCT Web Documentation - - - Sections: - - - diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000000..dbb1d36220 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,35 @@ +# Open MCT Web Documentation + +## Overview + + Documentation is provided to support the use and development of + Open MCT Web. It's recommended that before doing + any development with Open MCT Web you take some time to familiarize yourself + with the documentation below. + + Open MCT Web provides functionality out of the box, but it's also a platform for + building rich mission operations applications based on modern web technology. + The platform is configured declaratively, and defines conventions for + building on the provided capabilities by creating modular 'bundles' that + extend the platform at a variety of extension points. The details of how to + extend the platform are provided in the following documentation. + +## Sections + + * The [Architecture Overview](architecture/) describes the concepts used + throughout Open MCT Web, and gives a high level overview of the platform's design. + + * The [Developer's Guide](guide/) goes into more detail about how to use the + platform and the functionality that it provides. + + * The [Tutorials](tutorials/) give examples of extending the platform to add + functionality, + and integrate with data sources. + + * The [API](api/) document is generated from inline documentation + using [JSDoc](http://usejsdoc.org/), and describes the JavaScript objects and + functions that make up the software platform. + + * Finally, the [Development Process](process/) document describes the + Open MCT Web software development cycle. + \ No newline at end of file diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md index 26c47fefbd..1bda1d8848 100644 --- a/docs/src/tutorials/index.md +++ b/docs/src/tutorials/index.md @@ -1697,8 +1697,7 @@ Next, we utilize this functionality from the template: -''' - +``` __tutorials/bargraph/res/templates/bargraph.html__ Here, we utilize the functions we just provided from the controller to position diff --git a/package.json b/package.json index c96642129c..e753aa7bfb 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "jshint": "jshint platform example || exit 0", "watch": "karma start", "jsdoc": "jsdoc -c jsdoc.json -r -d target/docs/api", - "otherdoc": "node docs/gendocs.js --in docs/src --out target/docs", + "otherdoc": "node docs/gendocs.js --in docs/src --out target/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", "docs": "npm run jsdoc ; npm run otherdoc" }, "repository": { diff --git a/platform/commonUI/general/bundle.json b/platform/commonUI/general/bundle.json index df3c395091..f09e864ca7 100644 --- a/platform/commonUI/general/bundle.json +++ b/platform/commonUI/general/bundle.json @@ -116,10 +116,6 @@ "implementation": "controllers/GetterSetterController.js", "depends": [ "$scope" ] }, - { - "key": "SplitPaneController", - "implementation": "controllers/SplitPaneController.js" - }, { "key": "SelectorController", "implementation": "controllers/SelectorController.js", diff --git a/platform/commonUI/general/res/sass/_autoflow.scss b/platform/commonUI/general/res/sass/_autoflow.scss index 16fe7251f4..8a7564e7f5 100644 --- a/platform/commonUI/general/res/sass/_autoflow.scss +++ b/platform/commonUI/general/res/sass/_autoflow.scss @@ -35,24 +35,23 @@ } } - - .l-autoflow-header { bottom: auto; height: $headerH; line-height: $headerH; - min-width: $colW; - span { - vertical-align: middle; - } + min-width: $colW; + .t-last-update { + overflow: hidden; + } .s-btn.change-column-width { @include trans-prop-nice-fade(500ms); opacity: 0; } .l-filter { - margin-left: $interiorMargin; + display: block; + margin-right: $interiorMargin; input.t-filter-input { - width: 100px; + width: 150px; } } } @@ -127,4 +126,12 @@ } } } +} + +.frame { + &.child-frame.panel { + .autoflow .l-autoflow-header .l-filter { + display: none; + } + } } \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/_constants.scss b/platform/commonUI/general/res/sass/_constants.scss index c60767e4ae..b90cdfe6a9 100644 --- a/platform/commonUI/general/res/sass/_constants.scss +++ b/platform/commonUI/general/res/sass/_constants.scss @@ -71,7 +71,7 @@ $itemPadLR: 5px; $treeVCW: 10px; $treeTypeIconH: 1.4em; // was 16px $treeTypeIconHPx: 16px; -$treeTypeIconW: 20px; +$treeTypeIconW: 18px; $treeContextTriggerW: 20px; // Tabular $tabularHeaderH: 22px; //18px diff --git a/platform/commonUI/general/res/sass/_effects.scss b/platform/commonUI/general/res/sass/_effects.scss index aa2d477b66..ed740b9bcb 100644 --- a/platform/commonUI/general/res/sass/_effects.scss +++ b/platform/commonUI/general/res/sass/_effects.scss @@ -31,10 +31,6 @@ a.disabled { border-bottom: 1px solid rgba(#fff, 0.3); } -.outline { - @include boxOutline(); -} - .test-stripes { @include bgDiagonalStripes(); } diff --git a/platform/commonUI/general/res/sass/_icons.scss b/platform/commonUI/general/res/sass/_icons.scss index 3b41b31011..d24c4a74e8 100644 --- a/platform/commonUI/general/res/sass/_icons.scss +++ b/platform/commonUI/general/res/sass/_icons.scss @@ -73,31 +73,34 @@ } .l-icon-alert { - display: none !important; // Remove this when alerts are enabled + display: none !important; &:before { color: $colorAlert; content: "!"; } } -// NEW!! .t-item-icon { // Used in grid-item.html, tree-item, inspector location, more? @extend .ui-symbol; @extend .icon; - display: inline-block; line-height: normal; // This is Ok for the symbolsfont position: relative; + .t-item-icon-glyph { + position: absolute; + } &.l-icon-link { - &:before { - color: $colorIconLink; - content: "\f4"; - height: auto; width: auto; - position: absolute; - left: 0; top: 0; right: 0; bottom: 10%; - @include transform-origin(bottom, left); - @include transform(scale(0.3)); - z-index: 2; + .t-item-icon-glyph { + &:before { + color: $colorIconLink; + content: "\f4"; + height: auto; width: auto; + position: absolute; + left: 0; top: 0; right: 0; bottom: 10%; + @include transform-origin(bottom, left); + @include transform(scale(0.3)); + z-index: 2; + } } } } \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/_inspector.scss b/platform/commonUI/general/res/sass/_inspector.scss index b33f02ee37..4f707b8028 100644 --- a/platform/commonUI/general/res/sass/_inspector.scss +++ b/platform/commonUI/general/res/sass/_inspector.scss @@ -98,10 +98,19 @@ .inspector-location { .location-item { + $h: 1.2em; + @include box-sizing(border-box); cursor: pointer; display: inline-block; + line-height: $h; position: relative; padding: 2px 4px; + .t-object-label { + .t-item-icon { + height: $h; + width: 0.7rem; + } + } &:hover { background: $colorItemTreeHoverBg; color: $colorItemTreeHoverFg; @@ -116,6 +125,7 @@ display: inline-block; font-family: symbolsfont; font-size: 8px; + font-style: normal !important; line-height: inherit; margin-left: $interiorMarginSm; width: 4px; diff --git a/platform/commonUI/general/res/sass/_main.scss b/platform/commonUI/general/res/sass/_main.scss index f7f2489dc7..0376064759 100644 --- a/platform/commonUI/general/res/sass/_main.scss +++ b/platform/commonUI/general/res/sass/_main.scss @@ -60,6 +60,7 @@ @import "overlay/overlay"; @import "mobile/overlay/overlay"; @import "tree/tree"; +@import "object-label"; @import "mobile/tree"; @import "user-environ/frame"; @import "user-environ/top-bar"; diff --git a/platform/commonUI/general/res/sass/_mixins.scss b/platform/commonUI/general/res/sass/_mixins.scss index c0df207758..c3763e217b 100644 --- a/platform/commonUI/general/res/sass/_mixins.scss +++ b/platform/commonUI/general/res/sass/_mixins.scss @@ -306,7 +306,7 @@ @include desktop { @if $bgHov != none { &:not(.disabled):hover { - background: $bgHov; + @include background-image($bgHov); >.icon { color: lighten($ic, $ltGamma); } diff --git a/platform/commonUI/general/res/sass/_object-label.scss b/platform/commonUI/general/res/sass/_object-label.scss new file mode 100644 index 0000000000..55bc5d5920 --- /dev/null +++ b/platform/commonUI/general/res/sass/_object-label.scss @@ -0,0 +1,69 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +// mct-representation surrounding an object-label key="'label'" +.rep-object-label { + @include flex-direction(row); + @include flex(1 1 auto); + height: inherit; + line-height: inherit; + min-width: 0; +} + +.t-object-label { + .t-item-icon { + margin-right: $interiorMargin; + } +} + +mct-representation { + &.s-status-pending { + .t-object-label { + .t-item-icon { + &:before { + $spinBW: 4px; + $spinD: 0; + @include spinner($spinBW); + content: ""; + display: block; + position: absolute; + left: 50%; + top: 50%; + padding: 30%; + width: $spinD; + height: $spinD; + } + .t-item-icon-glyph { + display: none; + } + } + .t-title-label { + font-style: italic; + opacity: 0.6; + } + } + } +} +.selected mct-representation.s-status-pending .t-object-label .t-item-icon:before { + border-color: rgba($colorItemTreeSelectedFg, 0.25); + border-top-color: rgba($colorItemTreeSelectedFg, 1.0); +} \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/controls/_messages.scss b/platform/commonUI/general/res/sass/controls/_messages.scss index 740df6ba8d..8bb8337d9c 100644 --- a/platform/commonUI/general/res/sass/controls/_messages.scss +++ b/platform/commonUI/general/res/sass/controls/_messages.scss @@ -37,6 +37,8 @@ } .status.block { + $transDelay: 1.5s; + $transSpeed: .25s; color: $colorStatusDefault; cursor: default; display: inline-block; @@ -44,13 +46,47 @@ .status-indicator, .label, .count { - //@include test(#00ff00); display: inline-block; vertical-align: top; } + + &.no-icon { + .status-indicator { + display: none; + } + } + + &.float-right { + float: right; + } + + &.subtle { + opacity: 0.5; + } .status-indicator { margin-right: $interiorMarginSm; } + + &:not(.no-collapse) { + .label { + // Max-width silliness is necessary for width transition + @include trans-prop-nice(max-width, $transSpeed, $transDelay); + overflow: hidden; + max-width: 0px; + } + &:hover { + .label { + @include trans-prop-nice(max-width, $transSpeed, 0s); + max-width: 450px; + width: auto; + } + .count { + @include trans-prop-nice(max-width, $transSpeed, 0s); + opacity: 0; + } + } + } + &.ok .status-indicator, &.info .status-indicator { color: $colorStatusInfo; @@ -63,26 +99,11 @@ &.error .status-indicator { color: $colorStatusError; } - .label { - // Max-width silliness is necessary for width transition - @include trans-prop-nice(max-width, .25s); - overflow: hidden; - max-width: 0px; - } .count { - @include trans-prop-nice(opacity, .25s); + @include trans-prop-nice(opacity, $transSpeed, $transDelay); font-weight: bold; opacity: 1; } - &:hover { - .label { - max-width: 450px; - width: auto; - } - .count { - opacity: 0; - } - } } /* Styles for messages and message banners */ diff --git a/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss b/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss index 86c23a266a..14abf7c817 100644 --- a/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss +++ b/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss @@ -24,21 +24,27 @@ 100% { transform: rotate(359deg); } } -@mixin wait-spinner2($b: 5px, $c: $colorAlt1) { +@mixin spinner($b: 5px) { @include keyframes(rotateCentered) { - 0% { transform: translateX(-50%) translateY(-50%) rotate(0deg); } - 100% { transform: translateX(-50%) translateY(-50%) rotate(359deg); } - } + 0% { @include transform(translateX(-50%) translateY(-50%) rotate(0deg)); } + 100% { @include transform(translateX(-50%) translateY(-50%) rotate(359deg)); } + } @include animation-name(rotateCentered); @include animation-duration(0.5s); @include animation-iteration-count(infinite); @include animation-timing-function(linear); + @include transform-origin(center); + border-style: solid; + border-width: $b; + @include border-radius(100%); +} + + +@mixin wait-spinner2($b: 5px, $c: $colorAlt1) { + @include spinner($b); + @include box-sizing(border-box); border-color: rgba($c, 0.25); border-top-color: rgba($c, 1.0); - border-style: solid; - border-width: 5px; - @include border-radius(100%); - @include box-sizing(border-box); display: block; position: absolute; height: 0; width: 0; diff --git a/platform/commonUI/general/res/sass/mobile/_constants.scss b/platform/commonUI/general/res/sass/mobile/_constants.scss index 1a794b29f6..7d0d6baa9a 100644 --- a/platform/commonUI/general/res/sass/mobile/_constants.scss +++ b/platform/commonUI/general/res/sass/mobile/_constants.scss @@ -31,7 +31,7 @@ $tabletItemH: floor($ueBrowseGridItemLg/3); /************************** MOBILE TREE MENU DIMENSIONS */ $mobileTreeItemH: 35px; -$mobileTreeItemIndent: 20px; +$mobileTreeItemIndent: 15px; $mobileTreeRightArrowW: 30px; /************************** DEVICE WIDTHS */ diff --git a/platform/commonUI/general/res/sass/mobile/_tree.scss b/platform/commonUI/general/res/sass/mobile/_tree.scss index f3862be742..22ed795ce8 100644 --- a/platform/commonUI/general/res/sass/mobile/_tree.scss +++ b/platform/commonUI/general/res/sass/mobile/_tree.scss @@ -30,25 +30,30 @@ } .tree-item, .search-result-item { - height: $mobileTreeItemH; - line-height: $mobileTreeItemH; - margin-bottom: 0px; + height: $mobileTreeItemH !important; + line-height: $mobileTreeItemH !important; + margin-bottom: 0px !important; .view-control { - //@include test(red); - position: absolute; - font-size: 1.1em; - height: $mobileTreeItemH; - line-height: inherit; - right: 0px; - width: $mobileTreeRightArrowW; - text-align: center; + font-size: 1.2em; + margin-right: 0; + order: 2; + width: $mobileTreeItemH; + &.has-children { + &:before { + content: "\7d"; + left: 50%; + @include transform(translateX(-50%) rotate(90deg)); + } + &.expanded:before { + @include transform(translateX(-50%) rotate(270deg)); + } + } } - - .label, .t-object-label { - left: 0; - right: $mobileTreeRightArrowW + $interiorMargin; // Allows tree item name to stop prior to the arrow line-height: inherit; + .t-item-icon.l-icon-link .t-item-icon-glyph:before { + bottom: 20%; // Shift up due to height of mobile menu items + } } } } diff --git a/platform/commonUI/general/res/sass/mobile/search/_search.scss b/platform/commonUI/general/res/sass/mobile/search/_search.scss index d37514a0a5..3ceadcbaed 100644 --- a/platform/commonUI/general/res/sass/mobile/search/_search.scss +++ b/platform/commonUI/general/res/sass/mobile/search/_search.scss @@ -1,5 +1,5 @@ @include phone { - .search { + .search-holder { .search-bar { // Hide menu-icon and adjust spacing when in phone mode .menu-icon { diff --git a/platform/commonUI/general/res/sass/search/_search.scss b/platform/commonUI/general/res/sass/search/_search.scss index 62b869706b..37043ae939 100644 --- a/platform/commonUI/general/res/sass/search/_search.scss +++ b/platform/commonUI/general/res/sass/search/_search.scss @@ -20,33 +20,92 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -.holder-search { - // Moved a lot of stuff in here to _filter.scss - // to generalize approach to search input controls. +.clear-icon, +.menu-icon { + cursor: pointer; + font-family: symbolsfont; + @include trans-prop-nice((opacity, color), 150ms); +} +.clear-icon { + // 'x' in circle icon + &:before { + content: '\e607'; + } +} + +.holder-search { $iconWidth: 20px; - $textInputHeight: 19px; - $iconEdgeM: 4px; - $iconD: $treeSearchInputBarH - ($iconEdgeM*2); .search-bar { + $textInputHeight: 19px; // This is equal to the default value, 19px + $iconEdgeM: 4px; + $iconD: $treeSearchInputBarH - ($iconEdgeM*2); font-size: 0.8em; max-width: 250px; position: relative; - input[type="search"] { + .search-input { height: $treeSearchInputBarH; line-height: $treeSearchInputBarH; - position: relative; - width: 100%; - padding-left: $iconD + $interiorMargin !important; - padding-right: ($iconD * 2) + ($interiorMargin * 2) !important; } - .clear-icon { - right: $iconD + $interiorMargin; + &:before, + .clear-icon, + .menu-icon { + @include box-sizing(border-box); + color: $colorInputIcon; + height: $iconD; + width: $iconD; + line-height: $iconD; + position: absolute; + text-align: center; + top: $iconEdgeM; + } + + .search-input { + position: relative; + width: 100%; + padding-left: $iconD + $interiorMargin !important; + padding-right: ($iconD * 2) + ($interiorMargin * 2) !important; + + // Make work for mct-control textfield + input { + width: inherit; // was 100% + } + } + + &:before { + // Magnify glass icon + content:'\4d'; + font-family: symbolsfont; + left: $interiorMarginSm; + @include trans-prop-nice(color, 250ms); + pointer-events: none; + z-index: 1; } + // Make icon lighten when hovering over search bar + &:hover:before { + color: pullForward($colorInputIcon, 10%); + } + + .clear-icon { + right: $iconD + $interiorMargin; + + // Icon is visible only when there is text input + visibility: hidden; + opacity: 0; + &.show { + visibility: visible; + opacity: 1; + } + + &:hover { + color: pullForward($colorInputIcon, 10%); + } + } + .menu-icon { // 'v' invoke menu icon &:before { content: '\76'; } @@ -69,7 +128,7 @@ } .active-filter-display { - $s: 0.65em; + $s: 0.7em; $p: $interiorMargin; @include box-sizing(border-box); line-height: 130%; @@ -88,7 +147,6 @@ .search-results { @include trans-prop-nice((opacity, visibility), 250ms); - margin-top: $interiorMarginLg; // Always include margin here to fend off the search input padding-right: $interiorMargin; .hint { margin-bottom: $interiorMarginLg; diff --git a/platform/commonUI/general/res/sass/tree/_tree.scss b/platform/commonUI/general/res/sass/tree/_tree.scss index 7f49e76905..d4a629cd6c 100644 --- a/platform/commonUI/general/res/sass/tree/_tree.scss +++ b/platform/commonUI/general/res/sass/tree/_tree.scss @@ -35,23 +35,35 @@ ul.tree { .tree-item, .search-result-item { $runningItemW: 0; + @extend .l-flex-row; @include box-sizing(border-box); @include border-radius($basicCr); @include single-transition(background-color, 0.25s); - display: block; font-size: 0.8rem; height: $menuLineH; line-height: $menuLineH; margin-bottom: $interiorMarginSm; + padding: 0 $interiorMarginSm; position: relative; .view-control { color: $colorItemTreeVC; - display: inline-block; - margin-left: $interiorMargin; - font-size: 0.75em; + font-size: 0.75em; + margin-right: $interiorMargin; + height: 100%; + line-height: inherit; width: $treeVCW; - $runningItemW: $interiorMargin + $treeVCW; + &.has-children { + &:before { + position: absolute; + @include trans-prop-nice(transform, 100ms); + content: "\3e"; + @include transform-origin(center); + } + &.expanded:before { + @include transform(rotate(90deg)); + } + } @include desktop { &:hover { color: $colorItemTreeVCHover !important; @@ -59,64 +71,17 @@ ul.tree { } } - .label, .t-object-label { - display: block; - @include absPosDefault(); line-height: $menuLineH; - .t-item-icon { @include txtShdwSubtle($shdwItemTreeIcon); font-size: $treeTypeIconH; color: $colorItemTreeIcon; - position: absolute; - left: $interiorMargin; - top: 50%; - width: $treeTypeIconH; - @include transform(translateY(-50%)); + width: $treeTypeIconW; } - - .type-icon { - //@include absPosDefault(0, false); - $d: $treeTypeIconH; - @include txtShdwSubtle($shdwItemTreeIcon); - font-size: $treeTypeIconH; - color: $colorItemTreeIcon; - left: $interiorMargin; - position: absolute; - @include verticalCenterBlock($menuLineHPx, $treeTypeIconHPx); - line-height: 100%; - right: auto; width: $treeTypeIconH; - - .icon { - &.l-icon-link, - &.l-icon-alert { - position: absolute; - z-index: 2; - } - &.l-icon-alert { - $d: 8px; - @include ancillaryIcon($d, $colorAlert); - top: 1px; - right: -2px; - } - &.l-icon-link { - $d: 8px; - @include ancillaryIcon($d, $colorIconLink); - left: -3px; - bottom: 0px; - } - } - } - .title-label, .t-title-label { - @include absPosDefault(); - display: block; - left: $runningItemW + ($interiorMargin * 3); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } + @include ellipsize(); + } } &.selected { @@ -126,12 +91,11 @@ ul.tree { color: $colorItemTreeSelectedVC; } .t-object-label .t-item-icon { - color: $colorItemTreeSelectedFg; //$colorItemTreeIconHover; + color: $colorItemTreeSelectedFg; } } &:not(.selected) { - // NOTE: [Mobile] Removed Hover on Mobile @include desktop { &:hover { background: $colorItemTreeHoverBg; @@ -160,6 +124,31 @@ ul.tree { } } +mct-representation { + &.s-status-pending { + .t-object-label { + .t-item-icon { + &:before { + $spinBW: 4px; + @include spinner($spinBW); + border-color: rgba($colorItemTreeIcon, 0.25); + border-top-color: rgba($colorItemTreeIcon, 1.0); + } + .t-item-icon-glyph { + display: none; + } + } + .t-title-label { + font-style: italic; + opacity: 0.6; + } + } + } +} +.selected mct-representation.s-status-pending .t-object-label .t-item-icon:before { + border-color: rgba($colorItemTreeSelectedFg, 0.25); + border-top-color: rgba($colorItemTreeSelectedFg, 1.0); +} .tree .tree-item, .search-results .s-status-editing .search-result-item { .t-object-label { diff --git a/platform/commonUI/general/res/sass/user-environ/_layout.scss b/platform/commonUI/general/res/sass/user-environ/_layout.scss index ea345beac7..0d91fdc7ed 100644 --- a/platform/commonUI/general/res/sass/user-environ/_layout.scss +++ b/platform/commonUI/general/res/sass/user-environ/_layout.scss @@ -302,6 +302,7 @@ .splitter-treeview, .holder-treeview-elements { opacity: 0; + pointer-events: none; } } @@ -333,6 +334,7 @@ .l-inspect, .splitter-inspect { opacity: 0; + pointer-events: none; } } } diff --git a/platform/commonUI/general/res/templates/containers/split-pane.html b/platform/commonUI/general/res/templates/containers/split-pane.html deleted file mode 100644 index 397c22314e..0000000000 --- a/platform/commonUI/general/res/templates/containers/split-pane.html +++ /dev/null @@ -1,30 +0,0 @@ - - -
-
-
-
-
\ No newline at end of file diff --git a/platform/commonUI/general/res/templates/indicator.html b/platform/commonUI/general/res/templates/indicator.html index e9be598b18..fb4a2f89c9 100644 --- a/platform/commonUI/general/res/templates/indicator.html +++ b/platform/commonUI/general/res/templates/indicator.html @@ -20,7 +20,6 @@ at runtime from the About dialog for additional information. --> -
- -{{type.getGlyph()}} -{{model.name}} - +
+
+
{{type.getGlyph()}}
+
+
{{model.name}}
+
diff --git a/platform/commonUI/general/res/templates/object-inspector.html b/platform/commonUI/general/res/templates/object-inspector.html index 5f3e4d522e..5d7d490341 100644 --- a/platform/commonUI/general/res/templates/object-inspector.html +++ b/platform/commonUI/general/res/templates/object-inspector.html @@ -20,61 +20,44 @@ at runtime from the About dialog for additional information. --> -
- -
-
-
Inspection
-
    -
  • - Properties -
    -
    {{ data.name }}
    -
    {{ data.value }}
    -
    -
  • -
  • - Location - - - - -
  • -
  • - Original Location - - - - -
  • -
-
-
- -
-
- Elements - - -
+
+
Inspection
+
    +
  • + Properties +
    +
    {{ data.name }}
    +
    {{ data.value }}
    - -
+ +
  • + Location + + + + +
  • +
  • + Original Location + + + + +
  • + +
    diff --git a/platform/commonUI/general/res/templates/tree-node.html b/platform/commonUI/general/res/templates/tree-node.html index 5659f69cbf..97b32e6336 100644 --- a/platform/commonUI/general/res/templates/tree-node.html +++ b/platform/commonUI/general/res/templates/tree-node.html @@ -26,42 +26,19 @@ ng-class="{selected: treeNode.isSelected()}" > - {{toggle.isActive() ? "v" : ">"}} - - - - - - } - 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; - } -); diff --git a/platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js b/platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js index 957df1b36d..78f5e973ff 100644 --- a/platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js +++ b/platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js @@ -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); + }); + + }); + }); } diff --git a/platform/commonUI/general/test/controllers/SplitPaneControllerSpec.js b/platform/commonUI/general/test/controllers/SplitPaneControllerSpec.js deleted file mode 100644 index ee502e2e4e..0000000000 --- a/platform/commonUI/general/test/controllers/SplitPaneControllerSpec.js +++ /dev/null @@ -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); - }); - - }); - } -); \ No newline at end of file diff --git a/platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js b/platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js index 9fa17763fe..1d8fe5e5ce 100644 --- a/platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js +++ b/platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js @@ -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(); + }); + }); }); } diff --git a/platform/commonUI/general/test/directives/MCTSplitPaneSpec.js b/platform/commonUI/general/test/directives/MCTSplitPaneSpec.js index 2d5d5ac5a0..0743f1a584 100644 --- a/platform/commonUI/general/test/directives/MCTSplitPaneSpec.js +++ b/platform/commonUI/general/test/directives/MCTSplitPaneSpec.js @@ -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(); + }); }); }); diff --git a/platform/commonUI/general/test/directives/MCTSplitterSpec.js b/platform/commonUI/general/test/directives/MCTSplitterSpec.js new file mode 100644 index 0000000000..3aae62ccc2 --- /dev/null +++ b/platform/commonUI/general/test/directives/MCTSplitterSpec.js @@ -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'); + }); + + }); + }); + }); + } +); diff --git a/platform/commonUI/general/test/suite.json b/platform/commonUI/general/test/suite.json index 777001174a..5f3cf8bc64 100644 --- a/platform/commonUI/general/test/suite.json +++ b/platform/commonUI/general/test/suite.json @@ -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", diff --git a/platform/commonUI/mobile/bundle.json b/platform/commonUI/mobile/bundle.json index 1ac308a84b..52a0d282bc 100644 --- a/platform/commonUI/mobile/bundle.json +++ b/platform/commonUI/mobile/bundle.json @@ -13,6 +13,12 @@ "implementation": "AgentService.js", "depends": [ "$window" ] } + ], + "runs": [ + { + "implementation": "DeviceClassifier.js", + "depends": [ "agentService", "$document" ] + } ] } } diff --git a/platform/commonUI/mobile/src/AgentService.js b/platform/commonUI/mobile/src/AgentService.js index 109520d8bd..3251950ff4 100644 --- a/platform/commonUI/mobile/src/AgentService.js +++ b/platform/commonUI/mobile/src/AgentService.js @@ -46,6 +46,7 @@ define( this.userAgent = userAgent; this.mobileName = matches[0]; this.$window = $window; + this.touchEnabled = ($window.ontouchstart !== undefined); } /** @@ -92,6 +93,14 @@ define( return !this.isPortrait(); }; + /** + * Check if the user's device supports a touch interface. + * @returns {boolean} true if touch is supported + */ + AgentService.prototype.isTouch = function () { + return this.touchEnabled; + }; + /** * Check if the user agent matches a certain named device, * as indicated by checking for a case-insensitive substring diff --git a/platform/commonUI/mobile/src/DeviceClassifier.js b/platform/commonUI/mobile/src/DeviceClassifier.js new file mode 100644 index 0000000000..ac275fa4d0 --- /dev/null +++ b/platform/commonUI/mobile/src/DeviceClassifier.js @@ -0,0 +1,59 @@ +/***************************************************************************** + * 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*/ + +define( + ['./DeviceMatchers'], + function (DeviceMatchers) { + 'use strict'; + + /** + * Runs at application startup and adds a subset of the following + * CSS classes to the body of the document, depending on device + * attributes: + * + * * `mobile`: Phones or tablets. + * * `phone`: Phones specifically. + * * `tablet`: Tablets specifically. + * * `desktop`: Non-mobile devices. + * * `portrait`: Devices in a portrait-style orientation. + * * `landscape`: Devices in a landscape-style orientation. + * * `touch`: Device supports touch events. + * + * @param {platform/commonUI/mobile.AgentService} agentService + * the service used to examine the user agent + * @param $document Angular's jqLite-wrapped document element + * @constructor + */ + function MobileClassifier(agentService, $document) { + var body = $document.find('body'); + Object.keys(DeviceMatchers).forEach(function (key) { + if (DeviceMatchers[key](agentService)) { + body.addClass(key); + } + }); + } + + return MobileClassifier; + + } +); \ No newline at end of file diff --git a/platform/commonUI/mobile/src/DeviceMatchers.js b/platform/commonUI/mobile/src/DeviceMatchers.js new file mode 100644 index 0000000000..9292625a79 --- /dev/null +++ b/platform/commonUI/mobile/src/DeviceMatchers.js @@ -0,0 +1,60 @@ +/***************************************************************************** + * 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"; + + /** + * An object containing key-value pairs, where keys are symbolic of + * device attributes, and values are functions that take the + * `agentService` as inputs and return boolean values indicating + * whether or not the current device has these attributes. + * + * For internal use by the mobile support bundle. + * + * @memberof platform/commonUI/mobile + * @private + */ + return { + mobile: function (agentService) { + return agentService.isMobile(); + }, + phone: function (agentService) { + return agentService.isPhone(); + }, + tablet: function (agentService) { + return agentService.isTablet(); + }, + desktop: function (agentService) { + return !agentService.isMobile(); + }, + portrait: function (agentService) { + return agentService.isPortrait(); + }, + landscape: function (agentService) { + return agentService.isLandscape(); + }, + touch: function (agentService) { + return agentService.isTouch(); + } + }; +}); \ No newline at end of file diff --git a/platform/commonUI/mobile/src/MCTDevice.js b/platform/commonUI/mobile/src/MCTDevice.js index ce418898ee..704f665a22 100644 --- a/platform/commonUI/mobile/src/MCTDevice.js +++ b/platform/commonUI/mobile/src/MCTDevice.js @@ -22,31 +22,10 @@ /*global define,Promise*/ define( - function () { + ['./DeviceMatchers'], + function (DeviceMatchers) { 'use strict'; - var DEVICE_MATCHERS = { - mobile: function (agentService) { - return agentService.isMobile(); - }, - phone: function (agentService) { - return agentService.isPhone(); - }, - tablet: function (agentService) { - return agentService.isTablet(); - }, - desktop: function (agentService) { - return !agentService.isMobile(); - }, - portrait: function (agentService) { - return agentService.isPortrait(); - }, - landscape: function (agentService) { - return agentService.isLandscape(); - } - }; - - /** * The `mct-device` directive, when applied as an attribute, * only includes the element when the device being used matches @@ -68,6 +47,7 @@ define( * * `desktop`: Non-mobile devices. * * `portrait`: Devices in a portrait-style orientation. * * `landscape`: Devices in a landscape-style orientation. + * * `touch`: Device supports touch events. * * @param {AgentService} agentService used to detect device type * based on information about the user agent @@ -77,7 +57,7 @@ define( function deviceMatches(tokens) { tokens = tokens || ""; return tokens.split(" ").every(function (token) { - var fn = DEVICE_MATCHERS[token]; + var fn = DeviceMatchers[token]; return fn && fn(agentService); }); } diff --git a/platform/commonUI/mobile/test/AgentServiceSpec.js b/platform/commonUI/mobile/test/AgentServiceSpec.js index 573e72ddfb..be5fdaf6c5 100644 --- a/platform/commonUI/mobile/test/AgentServiceSpec.js +++ b/platform/commonUI/mobile/test/AgentServiceSpec.js @@ -82,6 +82,15 @@ define( expect(agentService.isLandscape()).toBeFalsy(); }); + it("detects touch support", function () { + testWindow.ontouchstart = null; + expect(new AgentService(testWindow).isTouch()) + .toBe(true); + delete testWindow.ontouchstart; + expect(new AgentService(testWindow).isTouch()) + .toBe(false); + }); + it("allows for checking browser type", function () { testWindow.navigator.userAgent = "Chromezilla Safarifox"; agentService = new AgentService(testWindow); diff --git a/platform/commonUI/mobile/test/DeviceClassifierSpec.js b/platform/commonUI/mobile/test/DeviceClassifierSpec.js new file mode 100644 index 0000000000..951c06f25a --- /dev/null +++ b/platform/commonUI/mobile/test/DeviceClassifierSpec.js @@ -0,0 +1,112 @@ +/***************************************************************************** + * 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/DeviceClassifier", "../src/DeviceMatchers"], + function (DeviceClassifier, DeviceMatchers) { + "use strict"; + + var AGENT_SERVICE_METHODS = [ + 'isMobile', + 'isPhone', + 'isTablet', + 'isPortrait', + 'isLandscape', + 'isTouch' + ], + TEST_PERMUTATIONS = [ + [ 'isMobile', 'isPhone', 'isTouch', 'isPortrait' ], + [ 'isMobile', 'isPhone', 'isTouch', 'isLandscape' ], + [ 'isMobile', 'isTablet', 'isTouch', 'isPortrait' ], + [ 'isMobile', 'isTablet', 'isTouch', 'isLandscape' ], + [ 'isTouch' ], + [] + ]; + + describe("DeviceClassifier", function () { + var mockAgentService, + mockDocument, + mockBody; + + beforeEach(function () { + mockAgentService = jasmine.createSpyObj( + 'agentService', + AGENT_SERVICE_METHODS + ); + mockDocument = jasmine.createSpyObj( + '$document', + [ 'find' ] + ); + mockBody = jasmine.createSpyObj( + 'body', + [ 'addClass' ] + ); + mockDocument.find.andCallFake(function (sel) { + return sel === 'body' && mockBody; + }); + AGENT_SERVICE_METHODS.forEach(function (m) { + mockAgentService[m].andReturn(false); + }); + }); + + TEST_PERMUTATIONS.forEach(function (trueMethods) { + var summary = trueMethods.length === 0 ? + "device has no detected characteristics" : + "device " + (trueMethods.join(", ")); + + describe("when " + summary, function () { + var classifier; + + beforeEach(function () { + trueMethods.forEach(function (m) { + mockAgentService[m].andReturn(true); + }); + classifier = new DeviceClassifier( + mockAgentService, + mockDocument + ); + }); + + it("adds classes for matching, detected characteristics", function () { + Object.keys(DeviceMatchers).filter(function (m) { + return DeviceMatchers[m](mockAgentService); + }).forEach(function (key) { + expect(mockBody.addClass) + .toHaveBeenCalledWith(key); + }); + }); + + it("does not add classes for non-matching characteristics", function () { + Object.keys(DeviceMatchers).filter(function (m) { + return !DeviceMatchers[m](mockAgentService); + }).forEach(function (key) { + expect(mockBody.addClass) + .not.toHaveBeenCalledWith(key); + }); + }); + }); + }); + }); + } +); diff --git a/platform/commonUI/mobile/test/DeviceMatchersSpec.js b/platform/commonUI/mobile/test/DeviceMatchersSpec.js new file mode 100644 index 0000000000..df78e49a6a --- /dev/null +++ b/platform/commonUI/mobile/test/DeviceMatchersSpec.js @@ -0,0 +1,81 @@ +/***************************************************************************** + * 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/DeviceMatchers"], + function (DeviceMatchers) { + 'use strict'; + + describe("DeviceMatchers", function () { + var mockAgentService; + + beforeEach(function () { + mockAgentService = jasmine.createSpyObj( + 'agentService', + [ + 'isMobile', + 'isPhone', + 'isTablet', + 'isPortrait', + 'isLandscape', + 'isTouch' + ] + ); + }); + + it("detects when a device is a desktop device", function () { + mockAgentService.isMobile.andReturn(false); + expect(DeviceMatchers.desktop(mockAgentService)) + .toBe(true); + mockAgentService.isMobile.andReturn(true); + expect(DeviceMatchers.desktop(mockAgentService)) + .toBe(false); + }); + + function method(deviceType) { + return "is" + deviceType[0].toUpperCase() + deviceType.slice(1); + } + + [ + "mobile", + "phone", + "tablet", + "landscape", + "portrait", + "landscape", + "touch" + ].forEach(function (deviceType) { + it("detects when a device is a " + deviceType + " device", function () { + mockAgentService[method(deviceType)].andReturn(true); + expect(DeviceMatchers[deviceType](mockAgentService)) + .toBe(true); + mockAgentService[method(deviceType)].andReturn(false); + expect(DeviceMatchers[deviceType](mockAgentService)) + .toBe(false); + }); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/commonUI/mobile/test/suite.json b/platform/commonUI/mobile/test/suite.json index b56625efb4..e72079e835 100644 --- a/platform/commonUI/mobile/test/suite.json +++ b/platform/commonUI/mobile/test/suite.json @@ -1,4 +1,6 @@ [ "AgentService", + "DeviceClassifier", + "DeviceMatchers", "MCTDevice" ] diff --git a/platform/entanglement/src/services/CopyTask.js b/platform/entanglement/src/services/CopyTask.js index d20c233b42..97acf1b327 100644 --- a/platform/entanglement/src/services/CopyTask.js +++ b/platform/entanglement/src/services/CopyTask.js @@ -45,6 +45,7 @@ define( this.policyService = policyService; this.persisted = 0; this.clones = []; + this.idMap = {}; } function composeChild(child, parent, setLocation) { @@ -57,6 +58,8 @@ define( if (setLocation && child.getModel().location === undefined) { child.getModel().location = parent.getId(); } + + return child; } function cloneObjectModel(objectModel) { @@ -104,6 +107,35 @@ define( .then(function(){return self.firstClone;}); } + /** + * Update identifiers in a cloned object model (or part of + * a cloned object model) to reflect new identifiers after + * copying. + * @private + */ + CopyTask.prototype.rewriteIdentifiers = function (obj, idMap) { + function lookupValue(value) { + return (typeof value === 'string' && idMap[value]) || value; + } + + if (Array.isArray(obj)) { + obj.forEach(function (value, index) { + obj[index] = lookupValue(value); + this.rewriteIdentifiers(obj[index], idMap); + }, this); + } else if (obj && typeof obj === 'object') { + Object.keys(obj).forEach(function (key) { + var value = obj[key]; + obj[key] = lookupValue(value); + if (idMap[key]) { + delete obj[key]; + obj[idMap[key]] = value; + } + this.rewriteIdentifiers(value, idMap); + }, this); + } + }; + /** * Given an array of objects composed by a parent, clone them, then * add them to the parent. @@ -111,7 +143,8 @@ define( * @returns {*} */ CopyTask.prototype.copyComposees = function(composees, clonedParent, originalParent){ - var self = this; + var self = this, + idMap = {}; return (composees || []).reduce(function(promise, originalComposee){ //If the composee is composed of other @@ -119,13 +152,28 @@ define( return promise.then(function(){ // ...to recursively copy it (and its children) return self.copy(originalComposee, originalParent).then(function(clonedComposee){ + //Map the original composee's ID to that of its + // clone so that we can replace any references to it + // in the parent + idMap[originalComposee.getId()] = clonedComposee.getId(); + //Compose the child within its parent. Cloned // objects will need to also have their location // set, however linked objects will not. return composeChild(clonedComposee, clonedParent, clonedComposee !== originalComposee); }); });}, self.$q.when(undefined) - ); + ).then(function(){ + //Replace any references in the cloned parent to + // contained objects that have been composed with the + // Ids of the clones + self.rewriteIdentifiers(clonedParent.getModel(), idMap); + + //Add the clone to the list of clones that will + //be returned by this function + self.clones.push(clonedParent); + return clonedParent; + }); }; /** @@ -159,12 +207,7 @@ define( //Duplicate the object's children, and their children, and // so on down to the leaf nodes of the tree. //If it is a link, don't both with children - return self.copyComposees(composees, clone, originalObject).then(function (){ - //Add the clone to the list of clones that will - //be returned by this function - self.clones.push(clone); - return clone; - }); + return self.copyComposees(composees, clone, originalObject); }); } else { //Creating a link, no need to iterate children diff --git a/platform/entanglement/test/services/CopyTaskSpec.js b/platform/entanglement/test/services/CopyTaskSpec.js new file mode 100644 index 0000000000..b63c72d6d2 --- /dev/null +++ b/platform/entanglement/test/services/CopyTaskSpec.js @@ -0,0 +1,277 @@ +/***************************************************************************** + * 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,describe,beforeEach,it,jasmine,expect,spyOn */ + +define( + [ + '../../src/services/CopyTask', + '../DomainObjectFactory' + ], + function (CopyTask, domainObjectFactory) { + 'use strict'; + + var ID_A = "some-string-with-vaguely-uuidish-uniqueness", + ID_B = "some-other-similarly-unique-string"; + + function synchronousPromise(value) { + return (value && value.then) ? value : { + then: function (callback) { + return synchronousPromise(callback(value)); + } + }; + } + + describe("CopyTask", function () { + var mockDomainObject, + mockParentObject, + mockPolicyService, + mockQ, + mockDeferred, + testModel, + mockCallback, + counter, + cloneIds, + task; + + function makeMockCapabilities(childIds) { + var mockCapabilities = { + persistence: jasmine.createSpyObj( + 'persistence', + ['persist'] + ), + composition: jasmine.createSpyObj( + 'composition', + ['add', 'invoke'] + ), + instantiation: jasmine.createSpyObj( + 'instantiation', + ['instantiate', 'invoke'] + ) + }, + mockChildren = (childIds || []).map(function (id) { + return domainObjectFactory({ + id: id, + capabilities: makeMockCapabilities([]), + model: { originalId: id } + }); + }); + + mockCapabilities.persistence.persist + .andReturn(synchronousPromise(true)); + mockCapabilities.composition.add.andCallFake(function (obj) { + return synchronousPromise(obj); + }); + mockCapabilities.composition.invoke + .andReturn(synchronousPromise(mockChildren)); + mockCapabilities.instantiation.invoke + .andCallFake(function (model) { + var id = "some-id-" + counter; + cloneIds[model.originalId] = id; + counter += 1; + return domainObjectFactory({ + id: id, + model: model, + capabilities: makeMockCapabilities() + }); + }); + + return mockCapabilities; + } + + beforeEach(function () { + counter = 0; + cloneIds = {}; + + testModel = { + composition: [ ID_A, ID_B ], + someObj: {}, + someArr: [ ID_A, ID_B ], + objArr: [{"id": ID_A}, {"id": ID_B}], + singleElementArr: [ ID_A ] + }; + testModel.someObj[ID_A] = "some value"; + testModel.someObj.someProperty = ID_B; + + mockDomainObject = domainObjectFactory({ + capabilities: makeMockCapabilities(testModel.composition), + model: testModel + }); + mockParentObject = domainObjectFactory({ + capabilities: makeMockCapabilities() + }); + mockPolicyService = jasmine.createSpyObj( + 'policyService', + [ 'allow' ] + ); + mockQ = jasmine.createSpyObj('$q', ['when', 'defer', 'all']); + mockDeferred = jasmine.createSpyObj( + 'deferred', + [ 'notify', 'resolve', 'reject' ] + ); + + mockPolicyService.allow.andReturn(true); + + mockQ.when.andCallFake(synchronousPromise); + mockQ.defer.andReturn(mockDeferred); + mockQ.all.andCallFake(function (promises) { + return synchronousPromise(promises.map(function (promise) { + var value; + promise.then(function (v) { value = v; }); + return value; + })); + }); + + mockDeferred.resolve.andCallFake(function (value) { + mockDeferred.promise = synchronousPromise(value); + }); + + + }); + + + describe("produces models which", function () { + var model; + + beforeEach(function () { + task = new CopyTask( + mockDomainObject, + mockParentObject, + mockPolicyService, + mockQ + ); + + task.perform().then(function (clone) { + model = clone.getModel(); + }); + }); + + it("contain rewritten identifiers in arrays", function () { + expect(model.someArr) + .toEqual(testModel.someArr.map(function (id) { + return cloneIds[id]; + })); + }); + + it("contain rewritten identifiers in properties", function () { + expect(model.someObj.someProperty) + .toEqual(cloneIds[testModel.someObj.someProperty]); + }); + + + it("contain rewritten identifiers in property names", function () { + expect(model.someObj[cloneIds[ID_A]]) + .toEqual(testModel.someObj[ID_A]); + }); + + it("contain rewritten identifiers in single-element arrays", function () { + expect(model.singleElementArr) + .toEqual(testModel.singleElementArr.map(function (id) { + return cloneIds[id]; + })); + }); + }); + + describe("copies object trees with multiple references to the" + + " same object", function () { + var model, + mockDomainObjectB, + mockComposingObject, + composingObjectModel, + domainObjectClone, + domainObjectBClone; + + beforeEach(function () { + mockDomainObjectB = domainObjectFactory({ + capabilities: makeMockCapabilities(testModel.composition), + model: testModel + }); + composingObjectModel = { + name: 'mockComposingObject', + composition: [mockDomainObject.getId(), mockDomainObjectB.getId()] + }; + mockComposingObject = domainObjectFactory({ + capabilities: makeMockCapabilities(composingObjectModel.composition), + model: composingObjectModel + }); + + mockComposingObject.capabilities.composition.invoke.andReturn([mockDomainObject, mockDomainObjectB]); + task = new CopyTask( + mockComposingObject, + mockParentObject, + mockPolicyService, + mockQ + ); + + task.perform(); + domainObjectClone = task.clones[2]; + domainObjectBClone = task.clones[5]; + }); + + /** + * mockDomainObject and mockDomainObjectB have the same + * model with references to children ID_A and ID_B. Expect + * that after duplication the references should differ + * because they are each now referencing different child + * objects. This tests the issue reported in #428 + */ + it(" and correctly updates child identifiers in models ", function () { + var childA_ID = task.clones[0].getId(), + childB_ID = task.clones[1].getId(), + childC_ID = task.clones[3].getId(), + childD_ID = task.clones[4].getId(); + + expect(domainObjectClone.model.someArr[0]).toNotBe(domainObjectBClone.model.someArr[0]); + expect(domainObjectClone.model.someArr[0]).toBe(childA_ID); + expect(domainObjectBClone.model.someArr[0]).toBe(childC_ID); + expect(domainObjectClone.model.someArr[1]).toNotBe(domainObjectBClone.model.someArr[1]); + expect(domainObjectClone.model.someArr[1]).toBe(childB_ID); + expect(domainObjectBClone.model.someArr[1]).toBe(childD_ID); + expect(domainObjectClone.model.someObj.someProperty).toNotBe(domainObjectBClone.model.someObj.someProperty); + expect(domainObjectClone.model.someObj.someProperty).toBe(childB_ID); + expect(domainObjectBClone.model.someObj.someProperty).toBe(childD_ID); + + }); + + /** + * This a bug found in testathon when testing issue #428 + */ + it(" and correctly updates child identifiers in object" + + " arrays within models ", function () { + var childA_ID = task.clones[0].getId(), + childB_ID = task.clones[1].getId(), + childC_ID = task.clones[3].getId(), + childD_ID = task.clones[4].getId(); + + expect(domainObjectClone.model.objArr[0].id).not.toBe(ID_A); + expect(domainObjectClone.model.objArr[0].id).toBe(childA_ID); + expect(domainObjectClone.model.objArr[1].id).not.toBe(ID_B); + expect(domainObjectClone.model.objArr[1].id).toBe(childB_ID); + + }); + }); + + }); + + + } +); diff --git a/platform/entanglement/test/suite.json b/platform/entanglement/test/suite.json index 243da94c39..223e473629 100644 --- a/platform/entanglement/test/suite.json +++ b/platform/entanglement/test/suite.json @@ -7,6 +7,7 @@ "actions/SetPrimaryLocationAction", "policies/CrossSpacePolicy", "services/CopyService", + "services/CopyTask", "services/LinkService", "services/MoveService", "services/LocationService", diff --git a/platform/features/clock/src/actions/AbstractStartTimerAction.js b/platform/features/clock/src/actions/AbstractStartTimerAction.js index 8c1554965c..116a5e51f4 100644 --- a/platform/features/clock/src/actions/AbstractStartTimerAction.js +++ b/platform/features/clock/src/actions/AbstractStartTimerAction.js @@ -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 domainObject.useCapability('mutation', setTimestamp) + .then(doPersist); + }; return AbstractStartTimerAction; } diff --git a/platform/features/clock/src/actions/RestartTimerAction.js b/platform/features/clock/src/actions/RestartTimerAction.js index 8c8a942281..0c4e0e93f4 100644 --- a/platform/features/clock/src/actions/RestartTimerAction.js +++ b/platform/features/clock/src/actions/RestartTimerAction.js @@ -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()) diff --git a/platform/features/clock/src/actions/StartTimerAction.js b/platform/features/clock/src/actions/StartTimerAction.js index d7237c75e4..f005a9a46e 100644 --- a/platform/features/clock/src/actions/StartTimerAction.js +++ b/platform/features/clock/src/actions/StartTimerAction.js @@ -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()) diff --git a/platform/features/clock/src/controllers/ClockController.js b/platform/features/clock/src/controllers/ClockController.js index eba2a07e27..387c8d44ad 100644 --- a/platform/features/clock/src/controllers/ClockController.js +++ b/platform/features/clock/src/controllers/ClockController.js @@ -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,32 +71,32 @@ 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 () { - return "UTC"; - }, - /** - * Get the current time, as displayable text. - * @returns {string} - */ - text: function () { - return text; - }, - /** - * Get the text to display to qualify a time as AM or PM. - * @returns {string} - */ - ampm: function () { - return use24 ? '' : ampm; - } - }; } + /** + * Get the clock's time zone, as displayable text. + * @returns {string} + */ + ClockController.prototype.zone = function () { + return "UTC"; + }; + + /** + * Get the current time, as displayable text. + * @returns {string} + */ + ClockController.prototype.text = function () { + return this.textValue; + }; + + /** + * Get the text to display to qualify a time as AM or PM. + * @returns {string} + */ + ClockController.prototype.ampm = function () { + return this.use24 ? '' : this.ampmValue; + }; + return ClockController; } ); diff --git a/platform/features/clock/src/controllers/RefreshingController.js b/platform/features/clock/src/controllers/RefreshingController.js index 4853da0f57..30d4d7841e 100644 --- a/platform/features/clock/src/controllers/RefreshingController.js +++ b/platform/features/clock/src/controllers/RefreshingController.js @@ -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; diff --git a/platform/features/clock/src/controllers/TimerController.js b/platform/features/clock/src/controllers/TimerController.js index 6bde70dd29..3f596df673 100644 --- a/platform/features/clock/src/controllers/TimerController.js +++ b/platform/features/clock/src/controllers/TimerController.js @@ -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,51 +122,59 @@ define( active = false; }); - return { - /** - * Get the glyph to display for the start/restart button. - * @returns {string} glyph to display - */ - buttonGlyph: function () { - return relevantAction ? - 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 : ""; - }, - /** - * Perform the action associated with the start/restart button. - */ - clickButton: function () { - if (relevantAction) { - relevantAction.perform(); - updateObject($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; - }, - /** - * Get the text to display for the current timer value. - * @returns {string} current timer value - */ - text: function () { - return text; - } - }; + 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 + */ + 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 + */ + TimerController.prototype.buttonText = function () { + return this.relevantAction ? + this.relevantAction.getMetadata().name : ""; + }; + + + /** + * Perform the action associated with the start/restart button. + */ + 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 + */ + TimerController.prototype.sign = function () { + return this.signValue; + }; + + /** + * Get the text to display for the current timer value. + * @returns {string} current timer value + */ + TimerController.prototype.text = function () { + return this.textValue; + }; + return TimerController; } ); diff --git a/platform/features/clock/src/controllers/TimerFormatter.js b/platform/features/clock/src/controllers/TimerFormatter.js index bea92b38f4..e9ebb79a6b 100644 --- a/platform/features/clock/src/controllers/TimerFormatter.js +++ b/platform/features/clock/src/controllers/TimerFormatter.js @@ -37,45 +37,39 @@ 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) - function toWholeSeconds(duration) { - 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, - /** - * 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 - }; } + // Round this timestamp down to the second boundary + // (e.g. 1124ms goes down to 1000ms, -2400ms goes down to -3000ms) + function toWholeSeconds(duration) { + return Math.abs(Math.floor(duration / 1000) * 1000); + } + + /** + * 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 + */ + 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 + */ + TimerFormatter.prototype.long = function (duration) { + return moment.duration(toWholeSeconds(duration), 'ms') + .format(LONG_FORMAT, { trim: false }); + }; + return TimerFormatter; } ); diff --git a/platform/features/clock/src/indicators/ClockIndicator.js b/platform/features/clock/src/indicators/ClockIndicator.js index 194e22067c..a564ddf8d1 100644 --- a/platform/features/clock/src/indicators/ClockIndicator.js +++ b/platform/features/clock/src/indicators/ClockIndicator.js @@ -28,32 +28,40 @@ 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 ""; - }, - 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; } ); diff --git a/platform/features/clock/src/services/TickerService.js b/platform/features/clock/src/services/TickerService.js index 4da85133fc..371c9a010e 100644 --- a/platform/features/clock/src/services/TickerService.js +++ b/platform/features/clock/src/services/TickerService.js @@ -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 { - /** - * 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); - - // Provide immediate feedback - callback(last); - - // Provide a deregistration function - return function () { - callbacks = callbacks.filter(function (cb) { - return cb !== callback; - }); - }; - } - }; - + 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. + * + * @param {Function} callback callback to invoke + * @returns {Function} a function to unregister this listener + */ + TickerService.prototype.listen = function (callback) { + var self = this; + + self.callbacks.push(callback); + + // Provide immediate feedback + callback(this.last); + + // Provide a deregistration function + return function () { + self.callbacks = self.callbacks.filter(function (cb) { + return cb !== callback; + }); + }; + }; + return TickerService; } ); diff --git a/platform/features/layout/test/FixedControllerSpec.js b/platform/features/layout/test/FixedControllerSpec.js index 31d5e06659..b6842497e0 100644 --- a/platform/features/layout/test/FixedControllerSpec.js +++ b/platform/features/layout/test/FixedControllerSpec.js @@ -423,6 +423,95 @@ define( // Style should have been updated 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; + + mockHandle.getDatum.andReturn({}); + mockHandle.getTelemetryObjects().forEach(function (mockObject) { + var id = mockObject.getId(), + mockLimitCapability = + jasmine.createSpyObj('limit-' + id, ['evaluate']); + + mockObject.getCapability.andCallFake(function (key) { + return (key === 'limit') && mockLimitCapability; + }); + + mockLimitCapability.evaluate + .andReturn({ cssClass: 'alarm-' + id }); + }); + + // Initialize + mockScope.domainObject = mockDomainObject; + mockScope.model = testModel; + findWatch("domainObject")(mockDomainObject); + findWatch("model.modified")(1); + findWatch("model.composition")(mockScope.model.composition); + + // Invoke the subscription callback + mockHandler.handle.mostRecentCall.args[1](); + + // Get elements that controller is now exposing + elements = controller.getElements(); + + // Limit-based CSS classes should be available + expect(elements[0].cssClass).toEqual("alarm-a"); + expect(elements[1].cssClass).toEqual("alarm-b"); + expect(elements[2].cssClass).toEqual("alarm-c"); + }); + }); } ); diff --git a/platform/features/plot/test/PlotControllerSpec.js b/platform/features/plot/test/PlotControllerSpec.js index 8edab2fc6d..f9715d822a 100644 --- a/platform/features/plot/test/PlotControllerSpec.js +++ b/platform/features/plot/test/PlotControllerSpec.js @@ -285,6 +285,61 @@ define( fireWatch("axes[1].active.key", 'someNewKey'); 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(); + + mockHandle.getDatum.andReturn({}); + mockTelemetryObjects.forEach(function (mockObject, i) { + var id = 'object-' + i, + mockLimitCapability = + jasmine.createSpyObj('limit-' + id, ['evaluate']); + + mockObject.getId.andReturn(id); + mockObject.getCapability.andCallFake(function (key) { + return (key === 'limit') && mockLimitCapability; + }); + + mockLimitCapability.evaluate + .andReturn({ cssClass: 'alarm-' + id }); + }); + + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + mockHandler.handle.mostRecentCall.args[1](); + + mockTelemetryObjects.forEach(function (mockTelemetryObject) { + expect(controller.getLegendClass(mockTelemetryObject)) + .toEqual('alarm-' + mockTelemetryObject.getId()); + }); + }); }); } ); diff --git a/platform/features/plot/test/elements/PlotLimitTrackerSpec.js b/platform/features/plot/test/elements/PlotLimitTrackerSpec.js new file mode 100644 index 0000000000..1ba428115f --- /dev/null +++ b/platform/features/plot/test/elements/PlotLimitTrackerSpec.js @@ -0,0 +1,103 @@ +/***************************************************************************** + * 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/elements/PlotLimitTracker"], + function (PlotLimitTracker) { + "use strict"; + + describe("A plot's limit tracker", function () { + var mockHandle, + testRange, + mockTelemetryObjects, + testData, + mockLimitCapabilities, + tracker; + + beforeEach(function () { + testRange = "some-range"; + testData = {}; + mockHandle = jasmine.createSpyObj( + 'handle', + ['getTelemetryObjects', 'getDatum'] + ); + mockTelemetryObjects = ['a', 'b', 'c'].map(function (id, i) { + var mockTelemetryObject = jasmine.createSpyObj( + 'object-' + id, + [ 'getId', 'getCapability', 'getModel' ] + ), + mockLimitCapability = jasmine.createSpyObj( + 'limit-' + id, + [ 'evaluate' ] + ); + testData[id] = { id: id, value: i }; + mockTelemetryObject.getId.andReturn(id); + mockTelemetryObject.getCapability.andCallFake(function (key) { + return key === 'limit' && mockLimitCapability; + }); + mockLimitCapability.evaluate + .andReturn({ cssClass: 'alarm-' + id}); + return mockTelemetryObject; + }); + mockHandle.getTelemetryObjects.andReturn(mockTelemetryObjects); + mockHandle.getDatum.andCallFake(function (telemetryObject) { + return testData[telemetryObject.getId()]; + }); + + tracker = new PlotLimitTracker(mockHandle, testRange); + }); + + it("initially provides no limit state", function () { + mockTelemetryObjects.forEach(function (mockTelemetryObject) { + expect(tracker.getLegendClass(mockTelemetryObject)) + .toBeUndefined(); + }); + }); + + describe("when asked to update", function () { + beforeEach(function () { + tracker.update(); + }); + + it("evaluates limits using the limit capability", function () { + mockTelemetryObjects.forEach(function (mockTelemetryObject) { + var id = mockTelemetryObject.getId(), + mockLimit = + mockTelemetryObject.getCapability('limit'); + expect(mockLimit.evaluate) + .toHaveBeenCalledWith(testData[id], testRange); + }); + }); + + it("exposes legend classes returned by the limit capability", function () { + mockTelemetryObjects.forEach(function (mockTelemetryObject) { + var id = mockTelemetryObject.getId(); + expect(tracker.getLegendClass(mockTelemetryObject)) + .toEqual('alarm-' + id); + }); + }); + }); + + }); + } +); diff --git a/platform/features/plot/test/suite.json b/platform/features/plot/test/suite.json index 92dfcb1e8a..cec8798d77 100644 --- a/platform/features/plot/test/suite.json +++ b/platform/features/plot/test/suite.json @@ -6,6 +6,7 @@ "SubPlot", "SubPlotFactory", "elements/PlotAxis", + "elements/PlotLimitTracker", "elements/PlotLine", "elements/PlotLineBuffer", "elements/PlotPalette", diff --git a/platform/features/scrolling/test/RangeColumnSpec.js b/platform/features/scrolling/test/RangeColumnSpec.js index c100a9efa0..b77245bb82 100644 --- a/platform/features/scrolling/test/RangeColumnSpec.js +++ b/platform/features/scrolling/test/RangeColumnSpec.js @@ -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(); }); diff --git a/platform/representation/src/TemplatePrefetcher.js b/platform/representation/src/TemplatePrefetcher.js index 7dc05b052a..e15633694b 100644 --- a/platform/representation/src/TemplatePrefetcher.js +++ b/platform/representation/src/TemplatePrefetcher.js @@ -30,7 +30,7 @@ define( * @param {platform/representation.TemplateLinker} templateLinker * the `templateLinker` service, used to load and cache * template extensions - * @param {...{templateUrl: string}[]} extensions arrays + * @param {...Array.<{templateUrl: string}>} extensions arrays * of template or template-like extensions */ function TemplatePrefetcher(templateLinker, extensions) { diff --git a/platform/search/res/templates/search-item.html b/platform/search/res/templates/search-item.html index 0cd5b60f17..09c3657ac7 100644 --- a/platform/search/res/templates/search-item.html +++ b/platform/search/res/templates/search-item.html @@ -20,11 +20,12 @@ at runtime from the About dialog for additional information. --> -
    + ng-click="ngModel.selectedObject = domainObject" + class="l-flex-row flex-elem grows">
    \ No newline at end of file diff --git a/platform/search/res/templates/search.html b/platform/search/res/templates/search.html index df1879a34a..e3f72e78b5 100644 --- a/platform/search/res/templates/search.html +++ b/platform/search/res/templates/search.html @@ -20,10 +20,13 @@ at runtime from the About dialog for additional information. -->