Compare commits

...

17 Commits

Author SHA1 Message Date
3a40eeb14b catching any errors from a user canceling from dialog 2021-06-23 11:05:28 -07:00
2a1e322230 adding link to discussions in readme to encourage users to showcase their use of Open MCT (#3933)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-06-22 09:34:36 -07:00
300b98bd54 Disallow pan, zoom and pause/play controls in time strip view (#3936)
* Disallow pan and zoom when in time strip view

* Disable plot controls in time strip view
2021-06-22 09:25:12 -07:00
c946609d13 Added LGTM code quality badge (#3960)
We score an A, we should flaunt it! (We should also aim for A+).
2021-06-22 09:10:08 -07:00
7ca559fbe4 [Styles] add unit tests (#3557)
* unit tests for inspector styles feature
* add mock capability for local storage

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-22 06:50:49 -07:00
71392915c1 Mct3834 (#3962)
* remove redundant code

Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
2021-06-21 17:23:42 -07:00
2889e88a97 Styling for plot limits (#3917)
* Styling for plot limits and colors
* Updates to limit provider and css
* Change limits related CSS "*--upper" and "*--lower" to "*--upr" and
"*--lwr" for better parity with legacy naming;
* Refactor limit class to util
* Use new classes for sine wave generator for red-low and yellow-high
* Added modifier classes for right and below-aligned labels;
* Prevent label overlap of limits as much as possible
* Add border colors to limit labels for better visual ties to their lines
* Add documentation for limit level specification API change

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-21 16:22:28 -07:00
d56d176aac Issue #3834 Reimplement New Tab action in vanilla JS (#3876)
* [Reimplement] create new action plugin for issue #3834

Co-authored-by mariuszr mariusz.rosinski@gmail.com
Co-authored-by: Henry Hsu <henryhsu@henrys-air.lan>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-21 16:11:31 -07:00
925518c83f [Notebooks] Don't save images on the object (#3792)
* Create and store image data into new domain object of type 'notebookSnapshotImage'
* Reduced thumbnail size to 30px
* Image migration script for old notebooks.
* Saves thumbnail image on notebook instead of new object.
2021-06-21 15:42:33 -07:00
fa5aceb7b3 Bind method to 'this' so that its listeners are correctly unbound on destroy (#3948)
* Bind method to 'this' so that its listener can work correctly
* Bind this for toggling subscriptions as well
2021-06-21 10:44:59 -07:00
6755ef4641 Support for remote mutation of Notebooks with Couch DB (#3887)
* Update notebook automatically when modified by another user
* Don't persist selected and default page and section IDs on notebook object
* Fixing object synchronization bugs
* Adding unit tests
* Synchronize notebooks AND plans
* Removed observeEnabled flag

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-06-21 10:25:17 -07:00
333e8b5583 [Testing] Resolve all promises (#3829)
* all promises in test specs should be returned

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-18 16:06:15 -07:00
9d8a8b36d2 Move duplicate fixes (#3947)
* Changed text of form labels;
* Corrected case of "Location" in Duplicate action;
* changed from objects.mutate to objects.save for duplicate action name change
* handling cancel of move

Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
2021-06-17 14:04:47 -07:00
b484a4a959 Initial LighthouseCI Commit (#3906)
* Initial LighthouseCI Commit

Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-06-16 11:37:37 -07:00
64e7c62d98 [Style] check if there is an element to style before applying style (#3950)
* check if there is an element to style

* add mmgis (external plugin) type to style exclusion list

* revert b919cf9 to be fixed properly

* reduce code
2021-06-15 16:03:23 -07:00
6483fe2402 Prepare master for Sprint 1.7.4 (#3925) 2021-06-07 11:37:49 -07:00
a123889d6a Pre release for Sprint 1.7.3 (#3924)
* Revert "upgrade to webpack5 (#3871)" (#3907) (#3908)
* [Navigation Tree] Fix composition on closed folders and scrolling for items NOT in tree (#3920)
* Update package.json version and version documentation to include tags for npmjs

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2021-06-07 11:25:58 -07:00
86 changed files with 2498 additions and 804 deletions

18
.github/workflows/lighthouse.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: lighthouse
on:
workflow_dispatch:
inputs:
version:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.version }}
- uses: actions/setup-node@v1
- run: npm install && npm install -g @lhci/cli #Don't want to include this in our deps
- run: lhci autorun

3
.gitignore vendored
View File

@ -40,4 +40,7 @@ npm-debug.log
# karma reports # karma reports
report.*.json report.*.json
# Lighthouse reports
.lighthouseci
package-lock.json package-lock.json

14
API.md
View File

@ -595,9 +595,17 @@ section.
#### Limit Evaluators **draft** #### Limit Evaluators **draft**
Limit evaluators allow a telemetry integrator to define how limits should be Limit evaluators allow a telemetry integrator to define which limits exist for a
applied to telemetry from a given domain object. For an example of a limit telemetry endpoint and how limits should be applied to telemetry from a given domain object.
evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.
A limit evaluator can implement the `evalute` method which is used to define how limits
should be applied to telemetry and the `getLimits` method which is used to specify
what the limit values are for different limit levels.
Limit levels can be mapped to one of 5 colors for visualization:
`purple`, `red`, `orange`, `yellow` and `cyan`.
For an example of a limit evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.
### Telemetry Consumer APIs **draft** ### Telemetry Consumer APIs **draft**

View File

@ -1,9 +1,11 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) # Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript)
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data. Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/) Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
## See Open MCT in Action ## See Open MCT in Action
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/). Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).

View File

@ -131,7 +131,8 @@ numbers by the following process:
3. In `package.json` change package to be public (private: false) 3. In `package.json` change package to be public (private: false)
4. Test the package before publishing by doing `npm publish --dry-run` 4. Test the package before publishing by doing `npm publish --dry-run`
if necessary. if necessary.
5. Publish the package to the npmjs registry (e.g. `npm publish --access public`) 5. Publish the package to the npmjs registry (e.g. `npm publish --access public`)
NOTE: Use the `--tag unstable` flag to the npm publishj if this is a prerelease.
6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`) 6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`)
5. Update snapshot status in `package.json` 5. Update snapshot status in `package.json`
1. Create a new branch off the `master` branch. 1. Create a new branch off the `master` branch.

View File

@ -26,14 +26,26 @@ define([
) { ) {
var RED = { var PURPLE = {
sin: 2.2,
cos: 2.2
},
RED = {
sin: 0.9, sin: 0.9,
cos: 0.9 cos: 0.9
}, },
ORANGE = {
sin: 0.7,
cos: 0.7
},
YELLOW = { YELLOW = {
sin: 0.5, sin: 0.5,
cos: 0.5 cos: 0.5
}, },
CYAN = {
sin: 0.45,
cos: 0.45
},
LIMITS = { LIMITS = {
rh: { rh: {
cssClass: "is-limit--upr is-limit--red", cssClass: "is-limit--upr is-limit--red",
@ -94,32 +106,66 @@ define([
}; };
SinewaveLimitProvider.prototype.getLimits = function (domainObject) { SinewaveLimitProvider.prototype.getLimits = function (domainObject) {
return { return {
limits: function () { limits: function () {
return { return Promise.resolve({
WATCH: {
low: {
color: "cyan",
sin: -CYAN.sin,
cos: -CYAN.cos
},
high: {
color: "cyan",
...CYAN
}
},
WARNING: { WARNING: {
low: { low: {
cssClass: "is-limit--lwr is-limit--yellow", color: "yellow",
sin: -YELLOW.sin, sin: -YELLOW.sin,
cos: -YELLOW.cos cos: -YELLOW.cos
}, },
high: { high: {
cssClass: "is-limit--upr is-limit--yellow", color: "yellow",
...YELLOW ...YELLOW
} }
}, },
DISTRESS: { DISTRESS: {
low: { low: {
cssClass: "is-limit--lwr is-limit--red", color: "orange",
sin: -ORANGE.sin,
cos: -ORANGE.cos
},
high: {
color: "orange",
...ORANGE
}
},
CRITICAL: {
low: {
color: "red",
sin: -RED.sin, sin: -RED.sin,
cos: -RED.cos cos: -RED.cos
}, },
high: { high: {
cssClass: "is-limit--upr is-limit--red", color: "red",
...RED ...RED
} }
},
SEVERE: {
low: {
color: "purple",
sin: -PURPLE.sin,
cos: -PURPLE.cos
},
high: {
color: "purple",
...PURPLE
}
} }
}; });
} }
}; };
}; };

96
lighthouserc.yml Normal file
View File

@ -0,0 +1,96 @@
---
ci:
collect:
urls:
- http://localhost/
numberOfRuns: 5
settings:
onlyCategories:
- performance
- best-practices
upload:
target: temporary-public-storage
assert:
preset: lighthouse:recommended
assertions:
### Applicable assertions
bootup-time:
- warn
- minScore: 0.88 #Original value was calculated at 0.88
dom-size:
- error
- maxNumericValue: 200 #Original value was calculated at 188
first-contentful-paint:
- error
- minScore: 0.07 #Original value was calculated at 0.08
mainthread-work-breakdown:
- warn
- minScore: 0.8 #Original value was calculated at 0.8
unused-javascript:
- warn
- maxLength: 1
- error
- maxNumericValue: 2000 #Original value was calculated at 1855
unused-css-rules: warn
installable-manifest: warn
service-worker: warn
### Disabled seo, accessibility, and pwa assertions, below
categories:seo: 'off'
categories:accessibility: 'off'
categories:pwa: 'off'
accesskeys: 'off'
apple-touch-icon: 'off'
aria-allowed-attr: 'off'
aria-command-name: 'off'
aria-hidden-body: 'off'
aria-hidden-focus: 'off'
aria-input-field-name: 'off'
aria-meter-name: 'off'
aria-progressbar-name: 'off'
aria-required-attr: 'off'
aria-required-children: 'off'
aria-required-parent: 'off'
aria-roles: 'off'
aria-toggle-field-name: 'off'
aria-tooltip-name: 'off'
aria-treeitem-name: 'off'
aria-valid-attr: 'off'
aria-valid-attr-value: 'off'
button-name: 'off'
bypass: 'off'
canonical: 'off'
color-contrast: 'off'
content-width: 'off'
crawlable-anchors: 'off'
csp-xss: 'off'
font-display: 'off'
font-size: 'off'
maskable-icon: 'off'
heading-order: 'off'
hreflang: 'off'
html-has-lang: 'off'
html-lang-valid: 'off'
http-status-code: 'off'
image-alt: 'off'
input-image-alt: 'off'
is-crawlable: 'off'
label: 'off'
link-name: 'off'
link-text: 'off'
list: 'off'
listitem: 'off'
meta-description: 'off'
meta-refresh: 'off'
meta-viewport: 'off'
object-alt: 'off'
plugins: 'off'
robots-txt: 'off'
splash-screen: 'off'
tabindex: 'off'
tap-targets: 'off'
td-headers-attr: 'off'
th-has-data-cells: 'off'
themed-omnibox: 'off'
valid-lang: 'off'
video-caption: 'off'
viewport: 'off'

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "1.7.3-SNAPSHOT", "version": "1.7.4-SNAPSHOT",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
@ -41,10 +41,10 @@
"jsdoc": "^3.3.2", "jsdoc": "^3.3.2",
"karma": "5.1.1", "karma": "5.1.1",
"karma-chrome-launcher": "3.1.0", "karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "1.3.0",
"karma-cli": "2.0.0", "karma-cli": "2.0.0",
"karma-coverage": "2.0.3", "karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3", "karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "1.3.0",
"karma-html-reporter": "0.2.7", "karma-html-reporter": "0.2.7",
"karma-jasmine": "3.3.1", "karma-jasmine": "3.3.1",
"karma-sourcemap-loader": "0.3.7", "karma-sourcemap-loader": "0.3.7",
@ -60,7 +60,7 @@
"moment-timezone": "0.5.28", "moment-timezone": "0.5.28",
"node-bourbon": "^4.2.3", "node-bourbon": "^4.2.3",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"painterro": "^1.0.35", "painterro": "^1.2.56",
"printj": "^1.2.1", "printj": "^1.2.1",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"request": "^2.69.0", "request": "^2.69.0",

View File

@ -24,7 +24,6 @@ define([
"./src/navigation/NavigationService", "./src/navigation/NavigationService",
"./src/navigation/NavigateAction", "./src/navigation/NavigateAction",
"./src/navigation/OrphanNavigationHandler", "./src/navigation/OrphanNavigationHandler",
"./src/windowing/NewTabAction",
"./res/templates/browse.html", "./res/templates/browse.html",
"./res/templates/browse-object.html", "./res/templates/browse-object.html",
"./res/templates/browse/object-header.html", "./res/templates/browse/object-header.html",
@ -37,7 +36,6 @@ define([
NavigationService, NavigationService,
NavigateAction, NavigateAction,
OrphanNavigationHandler, OrphanNavigationHandler,
NewTabAction,
browseTemplate, browseTemplate,
browseObjectTemplate, browseObjectTemplate,
objectHeaderTemplate, objectHeaderTemplate,
@ -128,23 +126,6 @@ define([
"depends": [ "depends": [
"navigationService" "navigationService"
] ]
},
{
"key": "window",
"name": "Open In New Tab",
"implementation": NewTabAction,
"description": "Open in a new browser tab",
"category": [
"view-control",
"contextual"
],
"depends": [
"urlService",
"$window"
],
"group": "windowing",
"priority": 10,
"cssClass": "icon-new-window"
} }
], ],
"runs": [ "runs": [

View File

@ -1,75 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/windowing/NewTabAction"],
function (NewTabAction) {
describe("The new tab action", function () {
var actionSelected,
actionCurrent,
mockWindow,
mockContextCurrent,
mockContextSelected,
mockUrlService;
beforeEach(function () {
mockWindow = jasmine.createSpyObj("$window", ["open", "location"]);
// Context if the current object is selected
// For example, when the top right new tab
// button is clicked, the user is using the
// current domainObject
mockContextCurrent = jasmine.createSpyObj("context", ["domainObject"]);
// Context if the selected object is selected
// For example, when an object in the left
// tree is opened in a new tab using the
// context menu
mockContextSelected = jasmine.createSpyObj("context", ["selectedObject",
"domainObject"]);
// Mocks the urlService used to make the new tab's url from a
// domainObject and mode
mockUrlService = jasmine.createSpyObj("urlService", ["urlForNewTab"]);
// Action done using the current context or mockContextCurrent
actionCurrent = new NewTabAction(mockUrlService, mockWindow,
mockContextCurrent);
// Action done using the selected context or mockContextSelected
actionSelected = new NewTabAction(mockUrlService, mockWindow,
mockContextSelected);
});
it("new tab with current url is opened", function () {
actionCurrent.perform();
});
it("new tab with a selected url is opened", function () {
actionSelected.perform();
});
});
}
);

View File

@ -274,6 +274,7 @@ define([
this.install(ImageryPlugin.default()); this.install(ImageryPlugin.default());
this.install(this.plugins.FlexibleLayout()); this.install(this.plugins.FlexibleLayout());
this.install(this.plugins.GoToOriginalAction()); this.install(this.plugins.GoToOriginalAction());
this.install(this.plugins.OpenInNewTabAction());
this.install(this.plugins.ImportExport()); this.install(this.plugins.ImportExport());
this.install(this.plugins.WebPage()); this.install(this.plugins.WebPage());
this.install(this.plugins.Condition()); this.install(this.plugins.Condition());

View File

@ -48,12 +48,12 @@ define(
* Converts an HTML element into a PNG or JPG Blob. * Converts an HTML element into a PNG or JPG Blob.
* @private * @private
* @param {node} element that will be converted to an image * @param {node} element that will be converted to an image
* @param {string} type of image to convert the element to. * @param {object} options Image options.
* @returns {promise} * @returns {promise}
*/ */
ExportImageService.prototype.renderElement = function (element, imageType, className) { ExportImageService.prototype.renderElement = function (element, {imageType, className, thumbnailSize}) {
const self = this;
const dialogService = this.dialogService; const dialogService = this.dialogService;
const dialog = dialogService.showBlockingMessage({ const dialog = dialogService.showBlockingMessage({
title: "Capturing...", title: "Capturing...",
hint: "Capturing an image", hint: "Capturing an image",
@ -90,7 +90,16 @@ define(
dialog.dismiss(); dialog.dismiss();
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
return canvas.toBlob(resolve, mimeType); if (thumbnailSize) {
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
return canvas.toBlob(blob => resolve({
blob,
thumbnail
}), mimeType);
}
return canvas.toBlob(blob => resolve({ blob }), mimeType);
}); });
}, function (error) { }, function (error) {
console.log('error capturing image', error); console.log('error capturing image', error);
@ -109,6 +118,17 @@ define(
}); });
}; };
ExportImageService.prototype.getThumbnail = function (canvas, mimeType, size) {
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.setAttribute('width', size.width);
thumbnailCanvas.setAttribute('height', size.height);
const ctx = thumbnailCanvas.getContext('2d');
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
return thumbnailCanvas.toDataURL(mimeType);
};
/** /**
* Takes a screenshot of a DOM node and exports to JPG. * Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported * @param {node} element to be exported
@ -119,9 +139,13 @@ define(
ExportImageService.prototype.exportJPG = function (element, filename, className) { ExportImageService.prototype.exportJPG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename); const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, "jpg", className).then(function (img) { return this.renderElement(element, {
saveAs(img, processedFilename); imageType: 'jpg',
}); className
})
.then(function (img) {
saveAs(img.blob, processedFilename);
});
}; };
/** /**
@ -134,9 +158,13 @@ define(
ExportImageService.prototype.exportPNG = function (element, filename, className) { ExportImageService.prototype.exportPNG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename); const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, "png", className).then(function (img) { return this.renderElement(element, {
saveAs(img, processedFilename); imageType: 'png',
}); className
})
.then(function (img) {
saveAs(img.blob, processedFilename);
});
}; };
/** /**
@ -146,8 +174,12 @@ define(
* @returns {promise} * @returns {promise}
*/ */
ExportImageService.prototype.exportPNGtoSRC = function (element, className) { ExportImageService.prototype.exportPNGtoSRC = function (element, options) {
return this.renderElement(element, "png", className);
return this.renderElement(element, {
imageType: 'png',
...options
});
}; };
function replaceDotsWithUnderscores(filename) { function replaceDotsWithUnderscores(filename) {

View File

@ -45,6 +45,8 @@ function ObjectAPI(typeRegistry, openmct) {
this.rootProvider = new RootObjectProvider(this.rootRegistry); this.rootProvider = new RootObjectProvider(this.rootRegistry);
this.cache = {}; this.cache = {};
this.interceptorRegistry = new InterceptorRegistry(); this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
} }
/** /**
@ -404,11 +406,16 @@ ObjectAPI.prototype._toMutable = function (object) {
let provider = this.getProvider(identifier); let provider = this.getProvider(identifier);
if (provider !== undefined if (provider !== undefined
&& provider.observe !== undefined) { && provider.observe !== undefined
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
let unobserve = provider.observe(identifier, (updatedModel) => { let unobserve = provider.observe(identifier, (updatedModel) => {
mutableObject.$refresh(updatedModel); if (updatedModel.persisted > mutableObject.modified) {
//Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
//in rapid succession and intermediate persistence states are returned by the observe function.
mutableObject.$refresh(updatedModel);
}
}); });
mutableObject.$on('$destroy', () => { mutableObject.$on('$_destroy', () => {
unobserve(); unobserve();
}); });
} }

View File

@ -163,14 +163,22 @@ describe("The Object API", () => {
key: 'test-key' key: 'test-key'
}, },
name: 'test object', name: 'test object',
type: 'notebook',
otherAttribute: 'other-attribute-value', otherAttribute: 'other-attribute-value',
modified: 0,
persisted: 0,
objectAttribute: { objectAttribute: {
embeddedObject: { embeddedObject: {
embeddedKey: 'embedded-value' embeddedKey: 'embedded-value'
} }
} }
}; };
updatedTestObject = Object.assign({otherAttribute: 'changed-attribute-value'}, testObject); updatedTestObject = Object.assign({
otherAttribute: 'changed-attribute-value'
}, testObject);
updatedTestObject.modified = 1;
updatedTestObject.persisted = 1;
mockProvider = jasmine.createSpyObj("mock provider", [ mockProvider = jasmine.createSpyObj("mock provider", [
"get", "get",
"create", "create",
@ -182,6 +190,8 @@ describe("The Object API", () => {
mockProvider.observeObjectChanges.and.callFake(() => { mockProvider.observeObjectChanges.and.callFake(() => {
callbacks[0](updatedTestObject); callbacks[0](updatedTestObject);
callbacks.splice(0, 1); callbacks.splice(0, 1);
return () => {};
}); });
mockProvider.observe.and.callFake((id, callback) => { mockProvider.observe.and.callFake((id, callback) => {
if (callbacks.length === 0) { if (callbacks.length === 0) {
@ -189,6 +199,8 @@ describe("The Object API", () => {
} else { } else {
callbacks[0] = callback; callbacks[0] = callback;
} }
return () => {};
}); });
objectAPI.addProvider(TEST_NAMESPACE, mockProvider); objectAPI.addProvider(TEST_NAMESPACE, mockProvider);

View File

@ -567,21 +567,24 @@ define([
* @method limits returns a limits object of * @method limits returns a limits object of
* type { * type {
* level1: { * level1: {
* low: { key1: value1, key2: value2 }, * low: { key1: value1, key2: value2, color: <supportedColor> },
* high: { key1: value1, key2: value2 } * high: { key1: value1, key2: value2, color: <supportedColor> }
* }, * },
* level2: { * level2: {
* low: { key1: value1, key2: value2 }, * low: { key1: value1, key2: value2 },
* high: { key1: value1, key2: value2 } * high: { key1: value1, key2: value2 }
* } * }
* } * }
* supported colors are purple, red, orange, yellow and cyan
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.getLimits = function (domainObject) { TelemetryAPI.prototype.getLimits = function (domainObject) {
const provider = this.findLimitEvaluator(domainObject); const provider = this.findLimitEvaluator(domainObject);
if (!provider) { if (!provider || !provider.getLimits) {
return { return {
limits: function () {} limits: function () {
return Promise.resolve(undefined);
}
}; };
} }

View File

@ -73,7 +73,7 @@ describe('the plugin', function () {
}); });
it('provides a folder to hold plans', () => { it('provides a folder to hold plans', () => {
openmct.objects.get(identifier).then((object) => { return openmct.objects.get(identifier).then((object) => {
expect(object).toEqual({ expect(object).toEqual({
identifier, identifier,
type: 'folder', type: 'folder',
@ -83,7 +83,7 @@ describe('the plugin', function () {
}); });
it('provides composition for couch search folders', () => { it('provides composition for couch search folders', () => {
composition.load().then((objects) => { return composition.load().then((objects) => {
expect(objects.length).toEqual(2); expect(objects.length).toEqual(2);
}); });
}); });

View File

@ -67,10 +67,6 @@ describe("The LAD Table", () => {
// this setups up the app // this setups up the app
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
parent = document.createElement('div'); parent = document.createElement('div');
@ -90,7 +86,7 @@ describe("The LAD Table", () => {
}); });
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(appHolder); openmct.startHeadless();
}); });
afterEach(() => { afterEach(() => {
@ -113,7 +109,8 @@ describe("The LAD Table", () => {
beforeEach(() => { beforeEach(() => {
ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable); ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable);
ladTableCompositionCollection.load();
return ladTableCompositionCollection.load();
}); });
it("should accept telemetry producing objects", () => { it("should accept telemetry producing objects", () => {
@ -192,8 +189,6 @@ describe("The LAD Table", () => {
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]); await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick(); await Vue.nextTick();
return;
}); });
it("should show one row per object in the composition", () => { it("should show one row per object in the composition", () => {
@ -242,13 +237,6 @@ describe("The LAD Table Set", () => {
let ladPlugin; let ladPlugin;
let parent; let parent;
let child; let child;
let telemetryCount = 3;
let timeFormat = 'utc';
let mockTelemetry = getMockTelemetry({
count: telemetryCount,
format: timeFormat
});
let mockObj = getMockObjects({ let mockObj = getMockObjects({
objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry'] objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry']
@ -264,31 +252,22 @@ describe("The LAD Table Set", () => {
mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier); mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier);
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
parent = document.createElement('div'); parent = document.createElement('div');
child = document.createElement('div'); child = document.createElement('div');
parent.appendChild(child); parent.appendChild(child);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
ladPlugin = new LadPlugin(); ladPlugin = new LadPlugin();
openmct.install(ladPlugin); openmct.install(ladPlugin);
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
openmct.time.bounds({ openmct.time.bounds({
start: bounds.start, start: bounds.start,
end: bounds.end end: bounds.end
}); });
openmct.on('start', done); openmct.on('start', done);
openmct.start(appHolder); openmct.startHeadless();
}); });
afterEach(() => { afterEach(() => {
@ -301,6 +280,8 @@ describe("The LAD Table Set", () => {
}); });
it("should provide a lad table set view only for lad table set objects", () => { it("should provide a lad table set view only for lad table set objects", () => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
let ladTableSetView = applicableViews.find( let ladTableSetView = applicableViews.find(
@ -315,8 +296,11 @@ describe("The LAD Table Set", () => {
let ladTableSetCompositionCollection; let ladTableSetCompositionCollection;
beforeEach(() => { beforeEach(() => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet); ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);
ladTableSetCompositionCollection.load();
return ladTableSetCompositionCollection.load();
}); });
it("should accept lad table objects", () => { it("should accept lad table objects", () => {
@ -354,41 +338,17 @@ describe("The LAD Table Set", () => {
otherObj.ladTable.composition.push(mockObj.telemetry.identifier); otherObj.ladTable.composition.push(mockObj.telemetry.identifier);
mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier); mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier);
beforeEach(async () => { beforeEach(() => {
let telemetryRequestResolve; spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
let ladObjectResolve;
let anotherLadObjectResolve;
let telemetryRequestPromise = new Promise((resolve) => { spyOn(openmct.objects, 'get').and.callFake((obj) => {
telemetryRequestResolve = resolve;
});
let ladObjectPromise = new Promise((resolve) => {
ladObjectResolve = resolve;
});
let anotherLadObjectPromise = new Promise((resolve) => {
anotherLadObjectResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise;
});
openmct.objects.get.and.callFake((obj) => {
if (obj.key === 'lad-object') { if (obj.key === 'lad-object') {
ladObjectResolve(mockObj.ladObject); return Promise.resolve(mockObj.ladTable);
return ladObjectPromise;
} else if (obj.key === 'another-lad-object') { } else if (obj.key === 'another-lad-object') {
anotherLadObjectResolve(otherObj.ladObject); return Promise.resolve(otherObj.ladTable);
} else if (obj.key === 'telemetry-object') {
return anotherLadObjectPromise; return Promise.resolve(mockObj.telemetry);
} }
return Promise.resolve({});
}); });
openmct.time.bounds({ openmct.time.bounds({
@ -399,20 +359,19 @@ describe("The LAD Table Set", () => {
applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey); ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey);
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]); ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
ladTableSetView.show(child, true); ladTableSetView.show(child);
await Promise.all([telemetryRequestPromise, ladObjectPromise, anotherLadObjectPromise]); return Vue.nextTick();
await Vue.nextTick();
return;
}); });
it("should show one row per lad table object in the composition", () => { it("should show one row per lad table object in the composition", () => {
const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length; const ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);
expect(rowCount).toBe(mockObj.ladTableSet.composition.length); return ladTableSetCompositionCollection.load().then(() => {
pending(); const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length;
expect(rowCount).toBe(mockObj.ladTableSet.composition.length);
});
}); });
}); });
}); });

View File

@ -22,12 +22,14 @@
define( define(
[ [
"utils/testing",
"./URLIndicator", "./URLIndicator",
"./URLIndicatorPlugin", "./URLIndicatorPlugin",
"../../MCT", "../../MCT",
"zepto" "zepto"
], ],
function ( function (
testingUtils,
URLIndicator, URLIndicator,
URLIndicatorPlugin, URLIndicatorPlugin,
MCT, MCT,
@ -44,7 +46,7 @@ define(
beforeEach(function () { beforeEach(function () {
jasmine.clock().install(); jasmine.clock().install();
openmct = new MCT(); openmct = new testingUtils.createOpenMct();
spyOn(openmct.indicators, 'add'); spyOn(openmct.indicators, 'add');
spyOn($, 'ajax'); spyOn($, 'ajax');
$.ajax.and.callFake(function (options) { $.ajax.and.callFake(function (options) {
@ -55,6 +57,8 @@ define(
afterEach(function () { afterEach(function () {
$.ajax = defaultAjaxFunction; $.ajax = defaultAjaxFunction;
jasmine.clock().uninstall(); jasmine.clock().uninstall();
return testingUtils.resetApplicationState(openmct);
}); });
describe("on initialization", function () { describe("on initialization", function () {

View File

@ -28,8 +28,10 @@ import {
resetApplicationState, resetApplicationState,
spyOnBuiltins spyOnBuiltins
} from 'utils/testing'; } from 'utils/testing';
import Vue from 'vue';
describe("AutoflowTabularPlugin", () => { // TODO lots of its without expects
xdescribe("AutoflowTabularPlugin", () => {
let testType; let testType;
let testObject; let testObject;
let mockmct; let mockmct;
@ -51,7 +53,7 @@ describe("AutoflowTabularPlugin", () => {
}); });
afterEach(() => { afterEach(() => {
resetApplicationState(mockmct); return resetApplicationState(mockmct);
}); });
it("installs a view provider", () => { it("installs a view provider", () => {
@ -101,7 +103,7 @@ describe("AutoflowTabularPlugin", () => {
}); });
} }
beforeEach((done) => { beforeEach(() => {
callbacks = {}; callbacks = {};
spyOnBuiltins(['requestAnimationFrame']); spyOnBuiltins(['requestAnimationFrame']);
@ -180,7 +182,7 @@ describe("AutoflowTabularPlugin", () => {
view = provider.view(testObject); view = provider.view(testObject);
view.show(testContainer); view.show(testContainer);
return done(); return Vue.nextTick();
}); });
afterEach(() => { afterEach(() => {

View File

@ -27,15 +27,17 @@ export default class StyleRuleManager extends EventEmitter {
super(); super();
this.openmct = openmct; this.openmct = openmct;
this.callback = callback; this.callback = callback;
this.refreshData = this.refreshData.bind(this);
this.toggleSubscription = this.toggleSubscription.bind(this);
if (suppressSubscriptionOnEdit) { if (suppressSubscriptionOnEdit) {
this.openmct.editor.on('isEditing', this.toggleSubscription.bind(this)); this.openmct.editor.on('isEditing', this.toggleSubscription);
this.isEditing = this.openmct.editor.editing; this.isEditing = this.openmct.editor.editing;
} }
if (styleConfiguration) { if (styleConfiguration) {
this.initialize(styleConfiguration); this.initialize(styleConfiguration);
if (styleConfiguration.conditionSetIdentifier) { if (styleConfiguration.conditionSetIdentifier) {
this.openmct.time.on("bounds", this.refreshData.bind(this)); this.openmct.time.on("bounds", this.refreshData);
this.subscribeToConditionSet(); this.subscribeToConditionSet();
} else { } else {
this.applyStaticStyle(); this.applyStaticStyle();

View File

@ -21,6 +21,10 @@
margin-left: $interiorMargin; margin-left: $interiorMargin;
} }
&__value {
@include isLimit();
}
.c-frame & { .c-frame & {
@include abs(); @include abs();
border: 1px solid transparent; border: 1px solid transparent;

View File

@ -37,7 +37,15 @@ export default class DuplicateAction {
let duplicationTask = new DuplicateTask(this.openmct); let duplicationTask = new DuplicateTask(this.openmct);
let originalObject = objectPath[0]; let originalObject = objectPath[0];
let parent = objectPath[1]; let parent = objectPath[1];
let userInput = await this.getUserInput(originalObject, parent); let userInput;
try {
userInput = await this.getUserInput(originalObject, parent);
} catch (error) {
// user most likely canceled
return;
}
let newParent = userInput.location; let newParent = userInput.location;
let inNavigationPath = this.inNavigationPath(originalObject); let inNavigationPath = this.inNavigationPath(originalObject);
@ -71,7 +79,8 @@ export default class DuplicateAction {
updateNameCheck(object, name) { updateNameCheck(object, name) {
if (object.name !== name) { if (object.name !== name) {
this.openmct.objects.mutate(object, 'name', name); object.name = name;
this.openmct.objects.save(object);
} }
} }
@ -95,7 +104,7 @@ export default class DuplicateAction {
cssClass: "l-input-lg" cssClass: "l-input-lg"
}, },
{ {
name: "location", name: "Location",
cssClass: "grows", cssClass: "grows",
control: "locator", control: "locator",
validate: this.validate(object, parent), validate: this.validate(object, parent),

View File

@ -121,10 +121,9 @@ describe("The Duplicate Action plugin", () => {
describe("when moving an object to a new parent", () => { describe("when moving an object to a new parent", () => {
beforeEach(async (done) => { beforeEach(async () => {
duplicateTask = new DuplicateTask(openmct); duplicateTask = new DuplicateTask(openmct);
await duplicateTask.duplicate(parentObject, anotherParentObject); await duplicateTask.duplicate(parentObject, anotherParentObject);
done();
}); });
it("the duplicate child object's name (when not changing) should be the same as the original object", async () => { it("the duplicate child object's name (when not changing) should be the same as the original object", async () => {
@ -143,15 +142,15 @@ describe("The Duplicate Action plugin", () => {
}); });
describe("when a new name is provided for the duplicated object", () => { describe("when a new name is provided for the duplicated object", () => {
const NEW_NAME = 'New Name'; it("the name is updated", () => {
const NEW_NAME = 'New Name';
let childName;
beforeEach(() => {
duplicateTask = new DuplicateAction(openmct); duplicateTask = new DuplicateAction(openmct);
duplicateTask.updateNameCheck(parentObject, NEW_NAME); duplicateTask.updateNameCheck(parentObject, NEW_NAME);
});
it("the name is updated", () => { childName = parentObject.name;
let childName = parentObject.name;
expect(childName).toEqual(NEW_NAME); expect(childName).toEqual(NEW_NAME);
}); });
}); });

View File

@ -24,10 +24,15 @@ import {
resetApplicationState resetApplicationState
} from 'utils/testing'; } from 'utils/testing';
describe("the plugin", () => { describe("the goToOriginalAction plugin", () => {
let openmct; let openmct;
let goToFolderAction; let goToOriginalAction;
let mockRootFolder;
let mockSubFolder;
let mockSubSubFolder;
let mockObject;
let mockObjectPath; let mockObjectPath;
let hash;
beforeEach((done) => { beforeEach((done) => {
openmct = createOpenMct(); openmct = createOpenMct();
@ -35,7 +40,7 @@ describe("the plugin", () => {
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.startHeadless();
goToFolderAction = openmct.actions._allActions.goToOriginal; goToOriginalAction = openmct.actions._allActions.goToOriginal;
}); });
afterEach(() => { afterEach(() => {
@ -43,34 +48,153 @@ describe("the plugin", () => {
}); });
it('installs the go to folder action', () => { it('installs the go to folder action', () => {
expect(goToFolderAction).toBeDefined(); expect(goToOriginalAction).toBeDefined();
}); });
describe('when invoked', () => { describe('when invoked', () => {
beforeEach(() => { beforeEach(() => {
mockObjectPath = [{ mockRootFolder = getMockObject('mock-root');
name: 'mock folder', mockSubFolder = getMockObject('mock-sub');
type: 'folder', mockSubSubFolder = getMockObject('mock-sub-sub');
identifier: { mockObject = getMockObject('mock-table');
key: 'mock-folder',
namespace: ''
}
}];
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({
identifier: {
namespace: '',
key: 'test'
}
}));
goToFolderAction.invoke(mockObjectPath); mockObjectPath = [
mockObject,
mockSubSubFolder,
mockSubFolder,
mockRootFolder
];
spyOn(openmct.objects, 'get').and.callFake(identifier => {
const mockedObject = getMockObject(identifier);
return Promise.resolve(mockedObject);
});
spyOn(openmct.router, 'navigate').and.callFake(navigateTo => {
hash = navigateTo;
});
return goToOriginalAction.invoke(mockObjectPath);
}); });
it('goes to the original location', (done) => { it('goes to the original location', () => {
setTimeout(() => { const originalLocationHash = '#/browse/mock-root/mock-table';
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc');
done(); return waitForNavigation(() => {
}, 1500); return hash === originalLocationHash;
}).then(() => {
expect(hash).toEqual(originalLocationHash);
});
}); });
}); });
function waitForNavigation(navigated) {
return new Promise((resolve, reject) => {
const start = Date.now();
checkNavigated();
function checkNavigated() {
const elapsed = Date.now() - start;
if (navigated()) {
resolve();
} else if (elapsed >= jasmine.DEFAULT_TIMEOUT_INTERVAL - 1000) {
reject("didn't navigate in time");
} else {
setTimeout(checkNavigated);
}
}
});
}
function getMockObject(key) {
const id = typeof key === 'string' ? key : key.key;
const mockMCTObjects = {
"ROOT": {
"composition": [
{
"namespace": "",
"key": "mock-root"
}
],
"identifier": {
"namespace": "",
"key": "mock-root"
}
},
"mock-root": {
"composition": [
{
"namespace": "",
"key": "mock-sub"
},
{
"namespace": "",
"key": "mock-table"
}
],
"name": "root",
"type": "folder",
"id": "mock-root",
"location": "ROOT",
"identifier": {
"namespace": "",
"key": "mock-root"
}
},
"mock-sub": {
"composition": [
{
"namespace": "",
"key": "mock-sub-sub"
},
{
"namespace": "",
"key": "mock-table"
}
],
"name": "sub",
"type": "folder",
"location": "mock-root",
"identifier": {
"namespace": "",
"key": "mock-sub"
}
},
"mock-table": {
"composition": [],
"configuration": {
"columnWidths": {},
"hiddenColumns": {}
},
"name": "table",
"type": "table",
"location": "mock-root",
"identifier": {
"namespace": "",
"key": "mock-table"
}
},
"mock-sub-sub": {
"composition": [
{
"namespace": "",
"key": "mock-table"
}
],
"name": "sub sub",
"type": "folder",
"location": "mock-sub",
"identifier": {
"namespace": "",
"key": "mock-sub-sub"
}
}
};
return mockMCTObjects[id];
}
}); });

View File

@ -30,10 +30,6 @@ describe('the plugin', function () {
const TEST_NAMESPACE = 'test'; const TEST_NAMESPACE = 'test';
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
openmct.install(new InterceptorPlugin(openmct)); openmct.install(new InterceptorPlugin(openmct));
@ -46,7 +42,7 @@ describe('the plugin', function () {
element.appendChild(child); element.appendChild(child);
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(appHolder); openmct.startHeadless();
}); });
afterEach(() => { afterEach(() => {
@ -55,6 +51,7 @@ describe('the plugin', function () {
describe('the missingObjectInterceptor', () => { describe('the missingObjectInterceptor', () => {
let mockProvider; let mockProvider;
beforeEach(() => { beforeEach(() => {
mockProvider = jasmine.createSpyObj("mock provider", [ mockProvider = jasmine.createSpyObj("mock provider", [
"get" "get"
@ -63,27 +60,28 @@ describe('the plugin', function () {
openmct.objects.addProvider(TEST_NAMESPACE, mockProvider); openmct.objects.addProvider(TEST_NAMESPACE, mockProvider);
}); });
it('returns missing objects', (done) => { it('returns missing objects', () => {
const identifier = { const identifier = {
namespace: TEST_NAMESPACE, namespace: TEST_NAMESPACE,
key: 'hello' key: 'hello'
}; };
openmct.objects.get(identifier).then((testObject) => {
return openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({ expect(testObject).toEqual({
identifier, identifier,
type: 'unknown', type: 'unknown',
name: 'Missing: test:hello' name: 'Missing: test:hello'
}); });
done();
}); });
}); });
it('returns the My items object if not found', (done) => { it('returns the My items object if not found', () => {
const identifier = { const identifier = {
namespace: TEST_NAMESPACE, namespace: TEST_NAMESPACE,
key: 'mine' key: 'mine'
}; };
openmct.objects.get(identifier).then((testObject) => {
return openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({ expect(testObject).toEqual({
identifier, identifier,
"name": "My Items", "name": "My Items",
@ -91,7 +89,6 @@ describe('the plugin', function () {
"composition": [], "composition": [],
"location": "ROOT" "location": "ROOT"
}); });
done();
}); });
}); });

View File

@ -37,7 +37,14 @@ export default class MoveAction {
let oldParent = objectPath[1]; let oldParent = objectPath[1];
let dialogService = this.openmct.$injector.get('dialogService'); let dialogService = this.openmct.$injector.get('dialogService');
let dialogForm = this.getDialogForm(object, oldParent); let dialogForm = this.getDialogForm(object, oldParent);
let userInput = await dialogService.getUserInput(dialogForm, { name: object.name }); let userInput;
try {
userInput = await dialogService.getUserInput(dialogForm, { name: object.name });
} catch (err) {
// user canceled, most likely
return;
}
// if we need to update name // if we need to update name
if (object.name !== userInput.name) { if (object.name !== userInput.name) {
@ -104,13 +111,13 @@ export default class MoveAction {
{ {
key: "name", key: "name",
control: "textfield", control: "textfield",
name: "Folder Name", name: "Name",
pattern: "\\S+", pattern: "\\S+",
required: true, required: true,
cssClass: "l-input-lg" cssClass: "l-input-lg"
}, },
{ {
name: "location", name: "Location",
control: "locator", control: "locator",
validate: this.validate(object, parent), validate: this.validate(object, parent),
key: 'location' key: 'location'

View File

@ -19,8 +19,6 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import MoveActionPlugin from './plugin.js';
import MoveAction from './MoveAction.js';
import { import {
createOpenMct, createOpenMct,
resetApplicationState, resetApplicationState,
@ -37,10 +35,6 @@ describe("The Move Action plugin", () => {
// this setups up the app // this setups up the app
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
childObject = getMockObjects({ childObject = getMockObjects({
@ -73,11 +67,10 @@ describe("The Move Action plugin", () => {
} }
}).folder; }).folder;
// already installed by default, but never hurts, just adds to context menu
openmct.install(MoveActionPlugin());
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(appHolder); openmct.startHeadless();
moveAction = openmct.actions._allActions.move;
}); });
afterEach(() => { afterEach(() => {
@ -85,13 +78,12 @@ describe("The Move Action plugin", () => {
}); });
it("should be defined", () => { it("should be defined", () => {
expect(MoveActionPlugin).toBeDefined(); expect(moveAction).toBeDefined();
}); });
describe("when moving an object to a new parent and removing from the old parent", () => { describe("when moving an object to a new parent and removing from the old parent", () => {
beforeEach(() => { beforeEach(() => {
moveAction = new MoveAction(openmct);
moveAction.addToNewParent(childObject, anotherParentObject); moveAction.addToNewParent(childObject, anotherParentObject);
moveAction.removeFromOldParent(parentObject, childObject); moveAction.removeFromOldParent(parentObject, childObject);
}); });

View File

@ -79,7 +79,7 @@ describe("the plugin", () => {
spyOn(compositionAPI, 'get').and.returnValue(mockComposition); spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
newFolderAction.invoke(mockObjectPath); return newFolderAction.invoke(mockObjectPath);
}); });
it('gets user input for folder name', () => { it('gets user input for folder name', () => {

View File

@ -31,7 +31,7 @@
</div> </div>
<SearchResults v-if="search.length" <SearchResults v-if="search.length"
ref="searchResults" ref="searchResults"
:domain-object="internalDomainObject" :domain-object="domainObject"
:results="searchResults" :results="searchResults"
@changeSectionPage="changeSelectedSection" @changeSectionPage="changeSelectedSection"
@updateEntries="updateEntries" @updateEntries="updateEntries"
@ -43,15 +43,18 @@
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left" class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]" :class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:default-page-id="defaultPageId" :default-page-id="defaultPageId"
:selected-page-id="selectedPageId"
:default-section-id="defaultSectionId" :default-section-id="defaultSectionId"
:domain-object="internalDomainObject" :selected-section-id="selectedSectionId"
:page-title="internalDomainObject.configuration.pageTitle" :domain-object="domainObject"
:section-title="internalDomainObject.configuration.sectionTitle" :page-title="domainObject.configuration.pageTitle"
:section-title="domainObject.configuration.sectionTitle"
:sections="sections" :sections="sections"
:selected-section="selectedSection"
:sidebar-covers-entries="sidebarCoversEntries" :sidebar-covers-entries="sidebarCoversEntries"
@pagesChanged="pagesChanged" @pagesChanged="pagesChanged"
@selectPage="selectPage"
@sectionsChanged="sectionsChanged" @sectionsChanged="sectionsChanged"
@selectSection="selectSection"
@toggleNav="toggleNav" @toggleNav="toggleNav"
/> />
<div class="c-notebook__page-view"> <div class="c-notebook__page-view">
@ -61,10 +64,10 @@
></button> ></button>
<div class="c-notebook__page-view__path c-path"> <div class="c-notebook__page-view__path c-path">
<span class="c-notebook__path__section c-path__item"> <span class="c-notebook__path__section c-path__item">
{{ getSelectedSection() ? getSelectedSection().name : '' }} {{ selectedSection ? selectedSection.name : '' }}
</span> </span>
<span class="c-notebook__path__page c-path__item"> <span class="c-notebook__path__page c-path__item">
{{ getSelectedPage() ? getSelectedPage().name : '' }} {{ selectedPage ? selectedPage.name : '' }}
</span> </span>
</div> </div>
<div class="c-notebook__page-view__controls"> <div class="c-notebook__page-view__controls">
@ -115,9 +118,9 @@
<NotebookEntry v-for="entry in filteredAndSortedEntries" <NotebookEntry v-for="entry in filteredAndSortedEntries"
:key="entry.id" :key="entry.id"
:entry="entry" :entry="entry"
:domain-object="internalDomainObject" :domain-object="domainObject"
:selected-page="getSelectedPage()" :selected-page="selectedPage"
:selected-section="getSelectedSection()" :selected-section="selectedSection"
:read-only="false" :read-only="false"
@deleteEntry="deleteEntry" @deleteEntry="deleteEntry"
@updateEntry="updateEntry" @updateEntry="updateEntry"
@ -152,14 +155,19 @@ export default {
SearchResults, SearchResults,
Sidebar Sidebar
}, },
inject: ['openmct', 'domainObject', 'snapshotContainer'], inject: ['openmct', 'snapshotContainer'],
props: {
domainObject: {
type: Object,
required: true
}
},
data() { data() {
return { return {
defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '', selectedSectionId: this.getDefaultSectionId(),
defaultSectionId: getDefaultNotebook() ? getDefaultNotebook().section.id : '', selectedPageId: this.getDefaultPageId(),
defaultSort: this.domainObject.configuration.defaultSort, defaultSort: this.domainObject.configuration.defaultSort,
focusEntryId: null, focusEntryId: null,
internalDomainObject: this.domainObject,
search: '', search: '',
searchResults: [], searchResults: [],
showTime: 0, showTime: 0,
@ -168,9 +176,15 @@ export default {
}; };
}, },
computed: { computed: {
defaultPageId() {
return this.getDefaultPageId();
},
defaultSectionId() {
return this.getDefaultSectionId();
},
filteredAndSortedEntries() { filteredAndSortedEntries() {
const filterTime = Date.now(); const filterTime = Date.now();
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || []; const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
const hours = parseInt(this.showTime, 10); const hours = parseInt(this.showTime, 10);
const filteredPageEntriesByTime = hours const filteredPageEntriesByTime = hours
@ -185,22 +199,28 @@ export default {
return this.getPages() || []; return this.getPages() || [];
}, },
sections() { sections() {
return this.internalDomainObject.configuration.sections || []; return this.getSections();
}, },
selectedPage() { selectedPage() {
const pages = this.getPages(); const pages = this.getPages();
if (!pages) { const selectedPage = pages.find(page => page.id === this.selectedPageId);
return {};
if (selectedPage) {
return selectedPage;
} }
return pages.find(page => page.isSelected); if (!selectedPage && !pages.length) {
return undefined;
}
return pages[0];
}, },
selectedSection() { selectedSection() {
if (!this.sections.length) { if (!this.sections.length) {
return {}; return null;
} }
return this.sections.find(section => section.isSelected); return this.sections.find(section => section.id === this.selectedSectionId);
} }
}, },
watch: { watch: {
@ -210,16 +230,14 @@ export default {
}, },
beforeMount() { beforeMount() {
this.getSearchResults = debounce(this.getSearchResults, 500); this.getSearchResults = debounce(this.getSearchResults, 500);
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
}, },
mounted() { mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.formatSidebar(); this.formatSidebar();
this.setSectionAndPageFromUrl();
window.addEventListener('orientationchange', this.formatSidebar); window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener("hashchange", this.navigateToSectionPage, false); window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
this.openmct.router.on('change:params', this.changeSectionPage);
this.navigateToSectionPage();
}, },
beforeDestroy() { beforeDestroy() {
if (this.unlisten) { if (this.unlisten) {
@ -227,8 +245,7 @@ export default {
} }
window.removeEventListener('orientationchange', this.formatSidebar); window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener("hashchange", this.navigateToSectionPage); window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
this.openmct.router.off('change:params', this.changeSectionPage);
}, },
updated: function () { updated: function () {
this.$nextTick(() => { this.$nextTick(() => {
@ -284,14 +301,21 @@ export default {
this.sectionsChanged({ sections }); this.sectionsChanged({ sections });
this.resetSearch(); this.resetSearch();
}, },
setSectionAndPageFromUrl() {
let sectionId = this.getSectionIdFromUrl() || this.selectedSectionId;
let pageId = this.getPageIdFromUrl() || this.selectedPageId;
this.selectSection(sectionId);
this.selectPage(pageId);
},
createNotebookStorageObject() { createNotebookStorageObject() {
const notebookMeta = { const notebookMeta = {
name: this.internalDomainObject.name, name: this.domainObject.name,
identifier: this.internalDomainObject.identifier, identifier: this.domainObject.identifier,
link: this.getLinktoNotebook() link: this.getLinktoNotebook()
}; };
const page = this.getSelectedPage(); const page = this.selectedPage;
const section = this.getSelectedSection(); const section = this.selectedSection;
return { return {
notebookMeta, notebookMeta,
@ -300,8 +324,7 @@ export default {
}; };
}, },
deleteEntry(entryId) { deleteEntry(entryId) {
const self = this; const entryPos = getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
const entryPos = getEntryPosById(entryId, this.internalDomainObject, this.selectedSection, this.selectedPage);
if (entryPos === -1) { if (entryPos === -1) {
this.openmct.notifications.alert('Warning: unable to delete entry'); this.openmct.notifications.alert('Warning: unable to delete entry');
console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`); console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`);
@ -317,9 +340,9 @@ export default {
label: "Ok", label: "Ok",
emphasis: true, emphasis: true,
callback: () => { callback: () => {
const entries = getNotebookEntries(self.internalDomainObject, self.selectedSection, self.selectedPage); const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries.splice(entryPos, 1); entries.splice(entryPos, 1);
self.updateEntries(entries); this.updateEntries(entries);
dialog.dismiss(); dialog.dismiss();
} }
}, },
@ -395,6 +418,37 @@ export default {
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout); const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
this.sidebarCoversEntries = sidebarCoversEntries; this.sidebarCoversEntries = sidebarCoversEntries;
}, },
getDefaultPageId() {
let defaultPageId;
if (this.isDefaultNotebook()) {
defaultPageId = getDefaultNotebook().page.id;
} else {
const firstSection = this.getSections()[0];
defaultPageId = firstSection && firstSection.pages[0].id;
}
return defaultPageId;
},
isDefaultNotebook() {
const defaultNotebook = getDefaultNotebook();
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.notebookMeta.identifier;
return defaultNotebookIdentifier !== null
&& this.openmct.objects.areIdsEqual(defaultNotebookIdentifier, this.domainObject.identifier);
},
getDefaultSectionId() {
let defaultSectionId;
if (this.isDefaultNotebook()) {
defaultSectionId = getDefaultNotebook().section.id;
} else {
const firstSection = this.getSections()[0];
defaultSectionId = firstSection && firstSection.id;
}
return defaultSectionId;
},
getDefaultNotebookObject() { getDefaultNotebookObject() {
const oldNotebookStorage = getDefaultNotebook(); const oldNotebookStorage = getDefaultNotebook();
if (!oldNotebookStorage) { if (!oldNotebookStorage) {
@ -423,14 +477,17 @@ export default {
getSection(id) { getSection(id) {
return this.sections.find(s => s.id === id); return this.sections.find(s => s.id === id);
}, },
getSections() {
return this.domainObject.configuration.sections || [];
},
getSearchResults() { getSearchResults() {
if (!this.search.length) { if (!this.search.length) {
return []; return [];
} }
const output = []; const output = [];
const sections = this.internalDomainObject.configuration.sections; const sections = this.domainObject.configuration.sections;
const entries = this.internalDomainObject.configuration.entries; const entries = this.domainObject.configuration.entries;
const searchTextLower = this.search.toLowerCase(); const searchTextLower = this.search.toLowerCase();
const originalSearchText = this.search; const originalSearchText = this.search;
let sectionTrackPageHit; let sectionTrackPageHit;
@ -509,77 +566,25 @@ export default {
this.searchResults = output; this.searchResults = output;
}, },
getPages() { getPages() {
const selectedSection = this.getSelectedSection(); const selectedSection = this.selectedSection;
if (!selectedSection || !selectedSection.pages.length) { if (!selectedSection || !selectedSection.pages.length) {
return []; return [];
} }
return selectedSection.pages; return selectedSection.pages;
}, },
getSelectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
}
const selectedPage = pages.find(page => page.isSelected);
if (selectedPage) {
return selectedPage;
}
if (!selectedPage && !pages.length) {
return null;
}
pages[0].isSelected = true;
return pages[0];
},
getSelectedSection() {
if (!this.sections.length) {
return null;
}
return this.sections.find(section => section.isSelected);
},
navigateToSectionPage() {
let { pageId, sectionId } = this.openmct.router.getParams();
if (!pageId || !sectionId) {
sectionId = this.selectedSection.id;
pageId = this.selectedPage.id;
}
const sections = this.sections.map(s => {
s.isSelected = false;
if (s.id === sectionId) {
s.isSelected = true;
s.pages.forEach(p => p.isSelected = (p.id === pageId));
}
return s;
});
const selectedSectionId = this.selectedSection && this.selectedSection.id;
const selectedPageId = this.selectedPage && this.selectedPage.id;
if (selectedPageId === pageId && selectedSectionId === sectionId) {
return;
}
this.sectionsChanged({ sections });
},
newEntry(embed = null) { newEntry(embed = null) {
this.resetSearch(); this.resetSearch();
const notebookStorage = this.createNotebookStorageObject(); const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage); this.updateDefaultNotebook(notebookStorage);
const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed); const id = addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
this.focusEntryId = id; this.focusEntryId = id;
}, },
orientationChange() { orientationChange() {
this.formatSidebar(); this.formatSidebar();
}, },
pagesChanged({ pages = [], id = null}) { pagesChanged({ pages = [], id = null}) {
const selectedSection = this.getSelectedSection(); const selectedSection = this.selectedSection;
if (!selectedSection) { if (!selectedSection) {
return; return;
} }
@ -594,7 +599,6 @@ export default {
}); });
this.sectionsChanged({ sections }); this.sectionsChanged({ sections });
this.updateDefaultNotebookPage(pages, id);
}, },
removeDefaultClass(domainObject) { removeDefaultClass(domainObject) {
if (!domainObject) { if (!domainObject) {
@ -613,10 +617,10 @@ export default {
async updateDefaultNotebook(notebookStorage) { async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject(); const defaultNotebookObject = await this.getDefaultNotebookObject();
if (!defaultNotebookObject) { if (!defaultNotebookObject) {
setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject); setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) { } else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
this.removeDefaultClass(defaultNotebookObject); this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject); setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
} }
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) { if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
@ -636,7 +640,7 @@ export default {
const notebookStorage = getDefaultNotebook(); const notebookStorage = getDefaultNotebook();
if (!notebookStorage if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) { || notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
return; return;
} }
@ -645,7 +649,7 @@ export default {
if (!page && defaultNotebookPage.id === id) { if (!page && defaultNotebookPage.id === id) {
this.defaultSectionId = null; this.defaultSectionId = null;
this.defaultPageId = null; this.defaultPageId = null;
this.removeDefaultClass(this.internalDomainObject); this.removeDefaultClass(this.domainObject);
clearDefaultNotebook(); clearDefaultNotebook();
return; return;
@ -664,7 +668,7 @@ export default {
const notebookStorage = getDefaultNotebook(); const notebookStorage = getDefaultNotebook();
if (!notebookStorage if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) { || notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
return; return;
} }
@ -673,7 +677,7 @@ export default {
if (!section && defaultNotebookSection.id === id) { if (!section && defaultNotebookSection.id === id) {
this.defaultSectionId = null; this.defaultSectionId = null;
this.defaultPageId = null; this.defaultPageId = null;
this.removeDefaultClass(this.internalDomainObject); this.removeDefaultClass(this.domainObject);
clearDefaultNotebook(); clearDefaultNotebook();
return; return;
@ -686,50 +690,46 @@ export default {
setDefaultNotebookSection(section); setDefaultNotebookSection(section);
}, },
updateEntry(entry) { updateEntry(entry) {
const entries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage); const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const entryPos = getEntryPosById(entry.id, this.internalDomainObject, this.selectedSection, this.selectedPage); const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
entries[entryPos] = entry; entries[entryPos] = entry;
this.updateEntries(entries); this.updateEntries(entries);
}, },
updateEntries(entries) { updateEntries(entries) {
const configuration = this.internalDomainObject.configuration; const configuration = this.domainObject.configuration;
const notebookEntries = configuration.entries || {}; const notebookEntries = configuration.entries || {};
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries; notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
mutateObject(this.openmct, this.internalDomainObject, 'configuration.entries', notebookEntries); mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries);
}, },
updateInternalDomainObject(domainObject) { getPageIdFromUrl() {
this.internalDomainObject = domainObject; return this.openmct.router.getParams().pageId;
}, },
updateParams(sections) { getSectionIdFromUrl() {
const selectedSection = sections.find(s => s.isSelected); return this.openmct.router.getParams().sectionId;
if (!selectedSection) { },
return; syncUrlWithPageAndSection() {
}
const selectedPage = selectedSection.pages.find(p => p.isSelected);
if (!selectedPage) {
return;
}
const sectionId = selectedSection.id;
const pageId = selectedPage.id;
if (!sectionId || !pageId) {
return;
}
this.openmct.router.updateParams({ this.openmct.router.updateParams({
sectionId, pageId: this.selectedPageId,
pageId sectionId: this.selectedSectionId
}); });
}, },
sectionsChanged({ sections, id = null }) { sectionsChanged({ sections, id = null }) {
mutateObject(this.openmct, this.internalDomainObject, 'configuration.sections', sections); mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections);
this.updateParams(sections);
this.updateDefaultNotebookSection(sections, id); this.updateDefaultNotebookSection(sections, id);
},
selectPage(pageId) {
this.selectedPageId = pageId;
this.syncUrlWithPageAndSection();
},
selectSection(sectionId) {
this.selectedSectionId = sectionId;
const defaultPageId = this.selectedSection.pages[0].id;
this.selectPage(defaultPageId);
this.syncUrlWithPageAndSection();
} }
} }
}; };

View File

@ -4,7 +4,7 @@
class="c-ne__embed__snap-thumb" class="c-ne__embed__snap-thumb"
@click="openSnapshot()" @click="openSnapshot()"
> >
<img :src="embed.snapshot.src"> <img :src="thumbnailImage">
</div> </div>
<div class="c-ne__embed__info"> <div class="c-ne__embed__info">
<div class="c-ne__embed__name"> <div class="c-ne__embed__name">
@ -25,11 +25,14 @@
<script> <script>
import Moment from 'moment'; import Moment from 'moment';
import PopupMenu from './PopupMenu.vue';
import PreviewAction from '../../../ui/preview/PreviewAction'; import PreviewAction from '../../../ui/preview/PreviewAction';
import RemoveDialog from '../utils/removeDialog'; import RemoveDialog from '../utils/removeDialog';
import PainterroInstance from '../utils/painterroInstance'; import PainterroInstance from '../utils/painterroInstance';
import SnapshotTemplate from './snapshot-template.html'; import SnapshotTemplate from './snapshot-template.html';
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
import PopupMenu from './PopupMenu.vue';
import Vue from 'vue'; import Vue from 'vue';
export default { export default {
@ -59,6 +62,11 @@ export default {
computed: { computed: {
createdOn() { createdOn() {
return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss'); return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss');
},
thumbnailImage() {
return this.embed.snapshot.thumbnailImage
? this.embed.snapshot.thumbnailImage.src
: this.embed.snapshot.src;
} }
}, },
mounted() { mounted() {
@ -85,7 +93,7 @@ export default {
template: '<div id="snap-annotation"></div>' template: '<div id="snap-annotation"></div>'
}).$mount(); }).$mount();
const painterroInstance = new PainterroInstance(annotateVue.$el, this.updateSnapshot); const painterroInstance = new PainterroInstance(annotateVue.$el);
const annotateOverlay = this.openmct.overlays.overlay({ const annotateOverlay = this.openmct.overlays.overlay({
element: annotateVue.$el, element: annotateVue.$el,
size: 'large', size: 'large',
@ -102,10 +110,12 @@ export default {
{ {
label: 'Save', label: 'Save',
callback: () => { callback: () => {
painterroInstance.save(); painterroInstance.save((snapshotObject) => {
annotateOverlay.dismiss(); annotateOverlay.dismiss();
this.snapshotOverlay.dismiss(); this.snapshotOverlay.dismiss();
this.openSnapshot(); this.updateSnapshot(snapshotObject);
this.openSnapshotOverlay(snapshotObject.fullSizeImage.src);
});
} }
} }
], ],
@ -115,7 +125,19 @@ export default {
}); });
painterroInstance.intialize(); painterroInstance.intialize();
painterroInstance.show(this.embed.snapshot.src);
const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;
if (!fullSizeImageObjectIdentifier) {
// legacy image data stored in embed
painterroInstance.show(this.embed.snapshot.src);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
painterroInstance.show(object.configuration.fullSizeImageURL);
});
}, },
changeLocation() { changeLocation() {
const hash = this.embed.historicLink; const hash = this.embed.historicLink;
@ -159,12 +181,29 @@ export default {
removeDialog.show(); removeDialog.show();
}, },
openSnapshot() { openSnapshot() {
const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;
if (!fullSizeImageObjectIdentifier) {
// legacy image data stored in embed
this.openSnapshotOverlay(this.embed.snapshot.src);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
this.openSnapshotOverlay(object.configuration.fullSizeImageURL);
});
},
openSnapshotOverlay(src) {
const self = this; const self = this;
this.snapshot = new Vue({ this.snapshot = new Vue({
data: () => { data: () => {
return { return {
createdOn: this.createdOn, createdOn: this.createdOn,
embed: this.embed name: this.embed.name,
cssClass: this.embed.cssClass,
src
}; };
}, },
methods: { methods: {
@ -217,7 +256,9 @@ export default {
this.$emit('updateEmbed', embed); this.$emit('updateEmbed', embed);
}, },
updateSnapshot(snapshotObject) { updateSnapshot(snapshotObject) {
this.embed.snapshot = snapshotObject; this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
this.updateEmbed(this.embed); this.updateEmbed(this.embed);
} }
} }

View File

@ -62,7 +62,6 @@
<NotebookEmbed v-for="embed in entry.embeds" <NotebookEmbed v-for="embed in entry.embeds"
:key="embed.id" :key="embed.id"
:embed="embed" :embed="embed"
:entry="entry"
@removeEmbed="removeEmbed" @removeEmbed="removeEmbed"
@updateEmbed="updateEmbed" @updateEmbed="updateEmbed"
/> />
@ -254,6 +253,7 @@ export default {
}, },
removeEmbed(id) { removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id); const embedPosition = this.findPositionInArray(this.entry.embeds, id);
// TODO: remove notebook snapshot object using object remove API
this.entry.embeds.splice(embedPosition, 1); this.entry.embeds.splice(embedPosition, 1);
this.$emit('updateEntry', this.entry); this.$emit('updateEntry', this.entry);

View File

@ -6,6 +6,7 @@
> >
<Page ref="pageComponent" <Page ref="pageComponent"
:default-page-id="defaultPageId" :default-page-id="defaultPageId"
:selected-page-id="selectedPageId"
:page="page" :page="page"
:page-title="pageTitle" :page-title="pageTitle"
@deletePage="deletePage" @deletePage="deletePage"
@ -33,11 +34,13 @@ export default {
return ''; return '';
} }
}, },
selectedPageId: {
type: String,
required: true
},
domainObject: { domainObject: {
type: Object, type: Object,
default() { required: true
return {};
}
}, },
pages: { pages: {
type: Array, type: Array,
@ -66,7 +69,17 @@ export default {
} }
} }
}, },
watch: {
pages() {
if (!this.containsPage(this.selectedPageId)) {
this.selectPage(this.pages[0].id);
}
}
},
methods: { methods: {
containsPage(pageId) {
return this.pages.some(page => page.id === pageId);
},
deletePage(id) { deletePage(id) {
const selectedSection = this.sections.find(s => s.isSelected); const selectedSection = this.sections.find(s => s.isSelected);
const page = this.pages.find(p => p.id === id); const page = this.pages.find(p => p.id === id);
@ -78,37 +91,29 @@ export default {
const isPageSelected = selectedPage && selectedPage.id === id; const isPageSelected = selectedPage && selectedPage.id === id;
const isPageDefault = defaultpage && defaultpage.id === id; const isPageDefault = defaultpage && defaultpage.id === id;
const pages = this.pages.filter(s => s.id !== id); const pages = this.pages.filter(s => s.id !== id);
let selectedPageId;
if (isPageSelected && defaultpage) { if (isPageSelected && defaultpage) {
pages.forEach(s => { pages.forEach(s => {
s.isSelected = false; s.isSelected = false;
if (defaultpage && defaultpage.id === s.id) { if (defaultpage && defaultpage.id === s.id) {
s.isSelected = true; selectedPageId = s.id;
} }
}); });
} }
if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) { if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) {
pages[0].isSelected = true; selectedPageId = pages[0].id;
} }
this.$emit('updatePage', { this.$emit('updatePage', {
pages, pages,
id id
}); });
this.$emit('selectPage', selectedPageId);
}, },
selectPage(id) { selectPage(id) {
const pages = this.pages.map(page => { this.$emit('selectPage', id);
const isSelected = page.id === id;
page.isSelected = isSelected;
return page;
});
this.$emit('updatePage', {
pages,
id
});
// Add test here for whether or not to toggle the nav // Add test here for whether or not to toggle the nav
if (this.sidebarCoversEntries) { if (this.sidebarCoversEntries) {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="c-list__item js-list__item" <div class="c-list__item js-list__item"
:class="[{ 'is-selected': page.isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]" :class="[{ 'is-selected': isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
:data-id="page.id" :data-id="page.id"
@click="selectPage" @click="selectPage"
> >
@ -29,6 +29,10 @@ export default {
return ''; return '';
} }
}, },
selectedPageId: {
type: String,
required: true
},
page: { page: {
type: Object, type: Object,
required: true required: true
@ -46,6 +50,11 @@ export default {
removeActionString: `Delete ${this.pageTitle}` removeActionString: `Delete ${this.pageTitle}`
}; };
}, },
computed: {
isSelected() {
return this.selectedPageId === this.page.id;
}
},
watch: { watch: {
page(newPage) { page(newPage) {
this.toggleContentEditable(newPage); this.toggleContentEditable(newPage);
@ -73,7 +82,7 @@ export default {
this.$emit('deletePage', this.page.id); this.$emit('deletePage', this.page.id);
}, },
getRemoveDialog() { getRemoveDialog() {
const message = 'This action will delete this page and all of its entries. Do you want to continue?'; const message = 'Other users may be editing entries in this page, and deleting it is permanent. Do you want to continue?';
const options = { const options = {
name: this.removeActionString, name: this.removeActionString,
callback: this.deletePage.bind(this), callback: this.deletePage.bind(this),

View File

@ -4,13 +4,14 @@
:key="section.id" :key="section.id"
class="c-list__item-h" class="c-list__item-h"
> >
<sectionComponent ref="sectionComponent" <NotebookSection ref="sectionComponent"
:default-section-id="defaultSectionId" :default-section-id="defaultSectionId"
:section="section" :selected-section-id="selectedSectionId"
:section-title="sectionTitle" :section="section"
@deleteSection="deleteSection" :section-title="sectionTitle"
@renameSection="updateSection" @deleteSection="deleteSection"
@selectSection="selectSection" @renameSection="updateSection"
@selectSection="selectSection"
/> />
</li> </li>
</ul> </ul>
@ -19,11 +20,11 @@
<script> <script>
import { deleteNotebookEntries } from '../utils/notebook-entries'; import { deleteNotebookEntries } from '../utils/notebook-entries';
import { getDefaultNotebook } from '../utils/notebook-storage'; import { getDefaultNotebook } from '../utils/notebook-storage';
import sectionComponent from './SectionComponent.vue'; import SectionComponent from './SectionComponent.vue';
export default { export default {
components: { components: {
sectionComponent NotebookSection: SectionComponent
}, },
inject: ['openmct'], inject: ['openmct'],
props: { props: {
@ -33,6 +34,10 @@ export default {
return ''; return '';
} }
}, },
selectedSectionId: {
type: String,
required: true
},
domainObject: { domainObject: {
type: Object, type: Object,
default() { default() {
@ -53,12 +58,22 @@ export default {
} }
} }
}, },
watch: {
sections() {
if (!this.containsSection(this.selectedSectionId)) {
this.selectSection(this.sections[0].id);
}
}
},
methods: { methods: {
containsSection(sectionId) {
return this.sections.some(section => section.id === sectionId);
},
deleteSection(id) { deleteSection(id) {
const section = this.sections.find(s => s.id === id); const section = this.sections.find(s => s.id === id);
deleteNotebookEntries(this.openmct, this.domainObject, section); deleteNotebookEntries(this.openmct, this.domainObject, section);
const selectedSection = this.sections.find(s => s.isSelected); const selectedSection = this.sections.find(s => s.id === this.selectedSectionId);
const defaultNotebook = getDefaultNotebook(); const defaultNotebook = getDefaultNotebook();
const defaultSection = defaultNotebook && defaultNotebook.section; const defaultSection = defaultNotebook && defaultNotebook.section;
const isSectionSelected = selectedSection && selectedSection.id === id; const isSectionSelected = selectedSection && selectedSection.id === id;
@ -83,18 +98,8 @@ export default {
id id
}); });
}, },
selectSection(id, newSections) { selectSection(id) {
const currentSections = newSections || this.sections; this.$emit('selectSection', id);
const sections = currentSections.map(section => {
const isSelected = section.id === id;
section.isSelected = isSelected;
return section;
});
this.$emit('updateSection', {
sections,
id
});
}, },
updateSection(newSection) { updateSection(newSection) {
const id = newSection.id; const id = newSection.id;

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="c-list__item js-list__item" <div class="c-list__item js-list__item"
:class="[{ 'is-selected': section.isSelected, 'is-notebook-default' : (defaultSectionId === section.id) }]" :class="[{ 'is-selected': isSelected, 'is-notebook-default' : (defaultSectionId === section.id) }]"
:data-id="section.id" :data-id="section.id"
@click="selectSection" @click="selectSection"
> >
@ -13,9 +13,6 @@
</div> </div>
</template> </template>
<style lang="scss">
</style>
<script> <script>
import PopupMenu from './PopupMenu.vue'; import PopupMenu from './PopupMenu.vue';
import RemoveDialog from '../utils/removeDialog'; import RemoveDialog from '../utils/removeDialog';
@ -32,6 +29,10 @@ export default {
return ''; return '';
} }
}, },
selectedSectionId: {
type: String,
required: true
},
section: { section: {
type: Object, type: Object,
required: true required: true
@ -49,6 +50,11 @@ export default {
removeActionString: `Delete ${this.sectionTitle}` removeActionString: `Delete ${this.sectionTitle}`
}; };
}, },
computed: {
isSelected() {
return this.selectedSectionId === this.section.id;
}
},
watch: { watch: {
section(newSection) { section(newSection) {
this.toggleContentEditable(newSection); this.toggleContentEditable(newSection);
@ -76,7 +82,7 @@ export default {
this.$emit('deleteSection', this.section.id); this.$emit('deleteSection', this.section.id);
}, },
getRemoveDialog() { getRemoveDialog() {
const message = 'This action will delete this section and all of its pages and entries. Do you want to continue?'; const message = 'Other users may be editing entries in this section, and deleting it is permanent. Do you want to continue?';
const options = { const options = {
name: this.removeActionString, name: this.removeActionString,
callback: this.deleteSection.bind(this), callback: this.deleteSection.bind(this),

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="c-sidebar c-drawer c-drawer--align-left"> <div class="c-sidebar c-drawer c-drawer--align-left">
<div class="c-sidebar__pane"> <div class="c-sidebar__pane js-sidebar-sections">
<div class="c-sidebar__header-w"> <div class="c-sidebar__header-w">
<div class="c-sidebar__header"> <div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ sectionTitle }}</span> <span class="c-sidebar__header-label">{{ sectionTitle }}</span>
@ -15,14 +15,16 @@
</button> </button>
<SectionCollection class="c-sidebar__contents" <SectionCollection class="c-sidebar__contents"
:default-section-id="defaultSectionId" :default-section-id="defaultSectionId"
:selected-section-id="selectedSectionId"
:domain-object="domainObject" :domain-object="domainObject"
:sections="sections" :sections="sections"
:section-title="sectionTitle" :section-title="sectionTitle"
@updateSection="sectionsChanged" @updateSection="sectionsChanged"
@selectSection="selectSection"
/> />
</div> </div>
</div> </div>
<div class="c-sidebar__pane"> <div class="c-sidebar__pane js-sidebar-pages">
<div class="c-sidebar__header-w"> <div class="c-sidebar__header-w">
<div class="c-sidebar__header"> <div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ pageTitle }}</span> <span class="c-sidebar__header-label">{{ pageTitle }}</span>
@ -42,6 +44,7 @@
<PageCollection ref="pageCollection" <PageCollection ref="pageCollection"
class="c-sidebar__contents" class="c-sidebar__contents"
:default-page-id="defaultPageId" :default-page-id="defaultPageId"
:selected-page-id="selectedPageId"
:domain-object="domainObject" :domain-object="domainObject"
:pages="pages" :pages="pages"
:sections="sections" :sections="sections"
@ -49,6 +52,7 @@
:page-title="pageTitle" :page-title="pageTitle"
@toggleNav="toggleNav" @toggleNav="toggleNav"
@updatePage="pagesChanged" @updatePage="pagesChanged"
@selectPage="selectPage"
/> />
</div> </div>
</div> </div>
@ -73,12 +77,24 @@ export default {
return ''; return '';
} }
}, },
selectedPageId: {
type: String,
default() {
return '';
}
},
defaultSectionId: { defaultSectionId: {
type: String, type: String,
default() { default() {
return ''; return '';
} }
}, },
selectedSectionId: {
type: String,
default() {
return '';
}
},
domainObject: { domainObject: {
type: Object, type: Object,
default() { default() {
@ -113,7 +129,7 @@ export default {
}, },
computed: { computed: {
pages() { pages() {
const selectedSection = this.sections.find(section => section.isSelected); const selectedSection = this.sections.find(section => section.id === this.selectedSectionId);
return selectedSection && selectedSection.pages || []; return selectedSection && selectedSection.pages || [];
} }
@ -144,6 +160,7 @@ export default {
pages, pages,
id: newPage.id id: newPage.id
}); });
this.$emit('selectPage', newPage.id);
}, },
addSection() { addSection() {
const newSection = this.createNewSection(); const newSection = this.createNewSection();
@ -153,6 +170,8 @@ export default {
sections, sections,
id: newSection.id id: newSection.id
}); });
this.$emit('selectSection', newSection.id);
}, },
addNewPage(page) { addNewPage(page) {
const pages = this.pages.map(p => { const pages = this.pages.map(p => {
@ -208,6 +227,12 @@ export default {
id id
}); });
}, },
selectPage(pageId) {
this.$emit('selectPage', pageId);
},
selectSection(sectionId) {
this.$emit('selectSection', sectionId);
},
sectionsChanged({ sections, id }) { sectionsChanged({ sections, id }) {
this.$emit('sectionsChanged', { this.$emit('sectionsChanged', {
sections, sections,

View File

@ -4,9 +4,9 @@
<div class="l-browse-bar__start"> <div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w"> <div class="l-browse-bar__object-name--w">
<span class="c-object-label l-browse-bar__object-name" <span class="c-object-label l-browse-bar__object-name"
v-bind:class="embed.cssClass" v-bind:class="cssClass"
> >
<span class="c-object-label__name">{{ embed.name }}</span> <span class="c-object-label__name">{{ name }}</span>
</span> </span>
</div> </div>
</div> </div>
@ -40,7 +40,7 @@
<div <div
ref="snapshot-image" ref="snapshot-image"
class="c-notebook-snapshot__image" class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }" :style="{ backgroundImage: 'url(' + src + ')' }"
> >
</div> </div>
</div> </div>

View File

@ -1,3 +1,4 @@
export const NOTEBOOK_TYPE = 'notebook';
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED'; export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
export const NOTEBOOK_DEFAULT = 'DEFAULT'; export const NOTEBOOK_DEFAULT = 'DEFAULT';
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT'; export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';

View File

@ -2,18 +2,20 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction';
import Notebook from './components/Notebook.vue'; import Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue'; import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import SnapshotContainer from './snapshot-container'; import SnapshotContainer from './snapshot-container';
import Vue from 'vue';
let installed = false; import { notebookImageMigration } from '../notebook/utils/notebook-migration';
import { NOTEBOOK_TYPE } from './notebook-constants';
import Vue from 'vue';
export default function NotebookPlugin() { export default function NotebookPlugin() {
return function install(openmct) { return function install(openmct) {
if (installed) { if (openmct._NOTEBOOK_PLUGIN_INSTALLED) {
return; return;
} else {
openmct._NOTEBOOK_PLUGIN_INSTALLED = true;
} }
installed = true;
openmct.actions.register(new CopyToNotebookAction(openmct)); openmct.actions.register(new CopyToNotebookAction(openmct));
const notebookType = { const notebookType = {
@ -84,7 +86,20 @@ export default function NotebookPlugin() {
} }
] ]
}; };
openmct.types.addType('notebook', notebookType); openmct.types.addType(NOTEBOOK_TYPE, notebookType);
const notebookSnapshotImageType = {
name: 'Notebook Snapshot Image Storage',
description: 'Notebook Snapshot Image Storage object',
creatable: false,
initialize: domainObject => {
domainObject.configuration = {
fullSizeImageURL: undefined,
thumbnailImageURL: undefined
};
}
};
openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType);
const snapshotContainer = new SnapshotContainer(openmct); const snapshotContainer = new SnapshotContainer(openmct);
const notebookSnapshotIndicator = new Vue ({ const notebookSnapshotIndicator = new Vue ({
@ -123,10 +138,14 @@ export default function NotebookPlugin() {
}, },
provide: { provide: {
openmct, openmct,
domainObject,
snapshotContainer snapshotContainer
}, },
template: '<Notebook></Notebook>' data() {
return {
domainObject
};
},
template: '<Notebook :domain-object="domainObject"></Notebook>'
}); });
}, },
destroy() { destroy() {
@ -135,5 +154,16 @@ export default function NotebookPlugin() {
}; };
} }
}); });
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'notebook';
},
invoke: (identifier, domainObject) => {
notebookImageMigration(openmct, domainObject);
return domainObject;
}
});
}; };
} }

View File

@ -21,29 +21,32 @@
*****************************************************************************/ *****************************************************************************/
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing'; import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
import NotebookPlugin from './plugin'; import notebookPlugin from './plugin';
import Vue from 'vue'; import Vue from 'vue';
let openmct;
let notebookDefinition;
let notebookPlugin;
let element;
let child;
let appHolder;
const notebookDomainObject = {
identifier: {
key: 'notebook',
namespace: ''
},
type: 'notebook'
};
describe("Notebook plugin:", () => { describe("Notebook plugin:", () => {
beforeAll(done => { let openmct;
let notebookDefinition;
let element;
let child;
let appHolder;
let objectProviderObserver;
let notebookDomainObject;
beforeEach((done) => {
notebookDomainObject = {
identifier: {
key: 'notebook',
namespace: 'test-namespace'
},
type: 'notebook'
};
appHolder = document.createElement('div'); appHolder = document.createElement('div');
appHolder.style.width = '640px'; appHolder.style.width = '640px';
appHolder.style.height = '480px'; appHolder.style.height = '480px';
document.body.appendChild(appHolder);
openmct = createOpenMct(); openmct = createOpenMct();
@ -51,19 +54,16 @@ describe("Notebook plugin:", () => {
child = document.createElement('div'); child = document.createElement('div');
element.appendChild(child); element.appendChild(child);
notebookPlugin = new NotebookPlugin(); openmct.install(notebookPlugin());
openmct.install(notebookPlugin);
notebookDefinition = openmct.types.get('notebook').definition; notebookDefinition = openmct.types.get('notebook').definition;
notebookDefinition.initialize(notebookDomainObject); notebookDefinition.initialize(notebookDomainObject);
openmct.on('start', done); openmct.on('start', done);
openmct.start(appHolder); openmct.start(appHolder);
document.body.append(appHolder);
}); });
afterAll(() => { afterEach(() => {
appHolder.remove(); appHolder.remove();
return resetApplicationState(openmct); return resetApplicationState(openmct);
@ -80,39 +80,96 @@ describe("Notebook plugin:", () => {
describe("Notebook view:", () => { describe("Notebook view:", () => {
let notebookViewProvider; let notebookViewProvider;
let notebookView; let notebookView;
let notebookViewObject;
let mutableNotebookObject;
beforeEach(() => { beforeEach(() => {
const notebookViewObject = { notebookViewObject = {
...notebookDomainObject, ...notebookDomainObject,
id: "test-object", id: "test-object",
name: 'Notebook', name: 'Notebook',
configuration: { configuration: {
defaultSort: 'oldest', defaultSort: 'oldest',
entries: {}, entries: {
"test-section-1": {
"test-page-1": [{
"id": "entry-0",
"createdOn": 0,
"text": "First Test Entry",
"embeds": []
}, {
"id": "entry-1",
"createdOn": 0,
"text": "Second Test Entry",
"embeds": []
}]
}
},
pageTitle: 'Page', pageTitle: 'Page',
sections: [], sections: [{
"id": "test-section-1",
"isDefault": false,
"isSelected": false,
"name": "Test Section",
"pages": [{
"id": "test-page-1",
"isDefault": false,
"isSelected": false,
"name": "Test Page 1",
"pageTitle": "Page"
}, {
"id": "test-page-2",
"isDefault": false,
"isSelected": false,
"name": "Test Page 2",
"pageTitle": "Page"
}]
}, {
"id": "test-section-2",
"isDefault": false,
"isSelected": false,
"name": "Test Section 2",
"pages": [{
"id": "test-page-3",
"isDefault": false,
"isSelected": false,
"name": "Test Page 3",
"pageTitle": "Page"
}]
}],
sectionTitle: 'Section', sectionTitle: 'Section',
type: 'General' type: 'General'
} }
}; };
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
const notebookObject = { const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]);
name: 'Notebook View', notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'notebook-vue');
key: 'notebook-vue',
creatable: true
};
const applicableViews = openmct.objectViews.get(notebookViewObject, []); testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === notebookObject.key); openmct.objects.addProvider('test-namespace', testObjectProvider);
notebookView = notebookViewProvider.view(notebookViewObject); testObjectProvider.observe.and.returnValue(() => {});
notebookView.show(child); return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
mutableNotebookObject = mutableObject;
objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1];
notebookView = notebookViewProvider.view(mutableNotebookObject);
notebookView.show(child);
return Vue.nextTick();
});
return Vue.nextTick();
}); });
afterEach(() => { afterEach(() => {
notebookView.destroy(); notebookView.destroy();
openmct.objects.destroyMutable(mutableNotebookObject);
}); });
it("provides notebook view", () => { it("provides notebook view", () => {
@ -133,6 +190,114 @@ describe("Notebook plugin:", () => {
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
it("renders a row for each entry", () => {
const notebookEntryElements = element.querySelectorAll('.c-notebook__entry');
const firstEntryText = getEntryText(0);
expect(notebookEntryElements.length).toBe(2);
expect(firstEntryText.innerText).toBe('First Test Entry');
});
describe("synchronization", () => {
it("updates an entry when another user modifies it", () => {
expect(getEntryText(0).innerText).toBe("First Test Entry");
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(getEntryText(0).innerText).toBe("Modified entry text");
});
});
it("shows new entry when another user adds one", () => {
expect(allNotebookEntryElements().length).toBe(2);
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"].push({
"id": "entry-3",
"createdOn": 0,
"text": "Third Test Entry",
"embeds": []
});
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookEntryElements().length).toBe(3);
});
});
it("removes an entry when another user removes one", () => {
expect(allNotebookEntryElements().length).toBe(2);
let entries = notebookViewObject.configuration.entries["test-section-1"]["test-page-1"];
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookEntryElements().length).toBe(1);
});
});
it("updates the notebook when a user adds a page", () => {
const newPage = {
"id": "test-page-4",
"isDefault": false,
"isSelected": false,
"name": "Test Page 4",
"pageTitle": "Page"
};
expect(allNotebookPageElements().length).toBe(2);
notebookViewObject.configuration.sections[0].pages.push(newPage);
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookPageElements().length).toBe(3);
});
});
it("updates the notebook when a user removes a page", () => {
expect(allNotebookPageElements().length).toBe(2);
notebookViewObject.configuration.sections[0].pages.splice(0, 1);
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookPageElements().length).toBe(1);
});
});
it("updates the notebook when a user adds a section", () => {
const newSection = {
"id": "test-section-3",
"isDefault": false,
"isSelected": false,
"name": "Test Section 3",
"pages": [{
"id": "test-page-4",
"isDefault": false,
"isSelected": false,
"name": "Test Page 4",
"pageTitle": "Page"
}]
};
expect(allNotebookSectionElements().length).toBe(2);
notebookViewObject.configuration.sections.push(newSection);
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookSectionElements().length).toBe(3);
});
});
it("updates the notebook when a user removes a section", () => {
expect(allNotebookSectionElements().length).toBe(2);
notebookViewObject.configuration.sections.splice(0, 1);
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookSectionElements().length).toBe(1);
});
});
});
}); });
describe("Notebook Snapshots view:", () => { describe("Notebook Snapshots view:", () => {
@ -147,16 +312,22 @@ describe("Notebook plugin:", () => {
button.dispatchEvent(clickEvent); button.dispatchEvent(clickEvent);
} }
beforeAll(() => { beforeEach(() => {
snapshotIndicator = openmct.indicators.indicatorObjects snapshotIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element; .find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
element.append(snapshotIndicator); element.append(snapshotIndicator);
return Vue.nextTick(); return Vue.nextTick().then(() => {
drawerElement = document.querySelector('.l-shell__drawer');
});
}); });
afterAll(() => { afterEach(() => {
if (drawerElement) {
drawerElement.classList.remove('is-expanded');
}
snapshotIndicator.remove(); snapshotIndicator.remove();
snapshotIndicator = undefined; snapshotIndicator = undefined;
@ -166,16 +337,6 @@ describe("Notebook plugin:", () => {
} }
}); });
beforeEach(() => {
drawerElement = document.querySelector('.l-shell__drawer');
});
afterEach(() => {
if (drawerElement) {
drawerElement.classList.remove('is-expanded');
}
});
it("has Snapshots indicator", () => { it("has Snapshots indicator", () => {
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined; const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
expect(hasSnapshotIndicator).toBe(true); expect(hasSnapshotIndicator).toBe(true);
@ -219,4 +380,20 @@ describe("Notebook plugin:", () => {
expect(snapshotsText).toBe('Notebook Snapshots'); expect(snapshotsText).toBe('Notebook Snapshots');
}); });
}); });
function getEntryText(entryNumber) {
return element.querySelectorAll('.c-notebook__entry .c-ne__text')[entryNumber];
}
function allNotebookEntryElements() {
return element.querySelectorAll('.c-notebook__entry');
}
function allNotebookSectionElements() {
return element.querySelectorAll('.js-sidebar-sections .js-list__item');
}
function allNotebookPageElements() {
return element.querySelectorAll('.js-sidebar-pages .js-list__item');
}
}); });

View File

@ -1,6 +1,8 @@
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries'; import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
import { getDefaultNotebook, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage'; import { getDefaultNotebook, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants'; import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
import SnapshotContainer from './snapshot-container'; import SnapshotContainer from './snapshot-container';
export default class Snapshot { export default class Snapshot {
@ -14,12 +16,17 @@ export default class Snapshot {
capture(snapshotMeta, notebookType, domElement) { capture(snapshotMeta, notebookType, domElement) {
const exportImageService = this.openmct.$injector.get('exportImageService'); const exportImageService = this.openmct.$injector.get('exportImageService');
exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot')
.then(function (blob) { const options = {
className: 's-status-taking-snapshot',
thumbnailSize: DEFAULT_SIZE
};
exportImageService.exportPNGtoSRC(domElement, options)
.then(function ({blob, thumbnail}) {
const reader = new window.FileReader(); const reader = new window.FileReader();
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
reader.onloadend = function () { reader.onloadend = function () {
this._saveSnapShot(notebookType, reader.result, snapshotMeta); this._saveSnapShot(notebookType, reader.result, thumbnail, snapshotMeta);
}.bind(this); }.bind(this);
}.bind(this)); }.bind(this));
} }
@ -27,16 +34,23 @@ export default class Snapshot {
/** /**
* @private * @private
*/ */
_saveSnapShot(notebookType, imageUrl, snapshotMeta) { _saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) {
const snapshot = imageUrl ? { src: imageUrl } : ''; createNotebookImageDomainObject(this.openmct, fullSizeImageURL)
const embed = createNewEmbed(snapshotMeta, snapshot); .then(object => {
if (notebookType === NOTEBOOK_DEFAULT) { const thumbnailImage = { src: thumbnailImageURL || '' };
this._saveToDefaultNoteBook(embed); const snapshot = {
fullSizeImageObjectIdentifier: object.identifier,
thumbnailImage
};
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
this._saveToDefaultNoteBook(embed);
return; return;
} }
this._saveToNotebookSnapshots(embed); this._saveToNotebookSnapshots(embed);
});
} }
/** /**

View File

@ -112,7 +112,7 @@ let openmct;
let mockIdentifierService; let mockIdentifierService;
describe('Notebook Entries:', () => { describe('Notebook Entries:', () => {
beforeEach(done => { beforeEach(() => {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']); openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj( mockIdentifierService = jasmine.createSpyObj(
@ -134,8 +134,6 @@ describe('Notebook Entries:', () => {
'update' 'update'
])); ]));
window.localStorage.setItem('notebook-storage', null); window.localStorage.setItem('notebook-storage', null);
done();
}); });
afterEach(() => { afterEach(() => {
@ -150,12 +148,11 @@ describe('Notebook Entries:', () => {
expect(entries.length).toEqual(0); expect(entries.length).toEqual(0);
}); });
it('addNotebookEntry adds entry', (done) => { it('addNotebookEntry adds entry', () => {
const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => { const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => {
const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage); const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
expect(entries.length).toEqual(1); expect(entries.length).toEqual(1);
done();
unlisten(); unlisten();
}); });

View File

@ -0,0 +1,78 @@
import uuid from 'uuid';
export const DEFAULT_SIZE = {
width: 30,
height: 30
};
export function createNotebookImageDomainObject(openmct, fullSizeImageURL) {
const identifier = {
key: uuid(),
namespace: ''
};
const viewType = 'notebookSnapshotImage';
const object = {
name: 'Notebook Snapshot Image',
type: viewType,
identifier,
configuration: {
fullSizeImageURL
}
};
return new Promise((resolve, reject) => {
openmct.objects.save(object)
.then(result => {
if (result) {
resolve(object);
}
reject();
})
.catch(e => {
console.error(e);
reject();
});
});
}
export function getThumbnailURLFromCanvas(canvas, size = DEFAULT_SIZE) {
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.setAttribute('width', size.width);
thumbnailCanvas.setAttribute('height', size.height);
const ctx = thumbnailCanvas.getContext('2d');
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
return thumbnailCanvas.toDataURL('image/png');
}
export function getThumbnailURLFromimageUrl(imageUrl, size = DEFAULT_SIZE) {
return new Promise(resolve => {
const image = new Image();
const canvas = document.createElement('canvas');
canvas.width = size.width;
canvas.height = size.height;
image.onload = function () {
canvas.getContext('2d')
.drawImage(image, 0, 0, size.width, size.height);
resolve(canvas.toDataURL('image/png'));
};
image.src = imageUrl;
});
}
export function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) {
openmct.objects.get(identifier)
.then(domainObject => {
const configuration = domainObject.configuration;
configuration.fullSizeImageURL = fullSizeImage.src;
openmct.objects.mutate(domainObject, 'configuration', configuration);
});
}

View File

@ -0,0 +1,43 @@
import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl } from './notebook-image';
import { mutateObject } from './notebook-entries';
export function notebookImageMigration(openmct, domainObject) {
const configuration = domainObject.configuration;
const notebookEntries = configuration.entries;
const imageMigrationVer = configuration.imageMigrationVer;
if (imageMigrationVer && imageMigrationVer === 'v1') {
return;
}
configuration.imageMigrationVer = 'v1';
// to avoid muliple notebookImageMigration calls updating images.
mutateObject(openmct, domainObject, 'configuration', configuration);
configuration.sections.forEach(section => {
const sectionId = section.id;
section.pages.forEach(page => {
const pageId = page.id;
const notebookSection = notebookEntries && notebookEntries[sectionId] || {};
const pageEntries = notebookSection && notebookSection[pageId] || [];
pageEntries.forEach(entry => {
entry.embeds.forEach(async (embed) => {
const snapshot = embed.snapshot;
const fullSizeImageURL = snapshot.src;
if (fullSizeImageURL) {
const thumbnailImageURL = await getThumbnailURLFromimageUrl(fullSizeImageURL);
const notebookImageDomainObject = await createNotebookImageDomainObject(openmct, fullSizeImageURL);
embed.snapshot = {
fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier,
thumbnailImage: { src: thumbnailImageURL || '' }
};
mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries);
}
});
});
});
});
}

View File

@ -60,7 +60,7 @@ let openmct;
let mockIdentifierService; let mockIdentifierService;
describe('Notebook Storage:', () => { describe('Notebook Storage:', () => {
beforeEach((done) => { beforeEach(() => {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']); openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj( mockIdentifierService = jasmine.createSpyObj(
@ -79,7 +79,6 @@ describe('Notebook Storage:', () => {
'create', 'create',
'update' 'update'
])); ]));
done();
}); });
afterEach(() => { afterEach(() => {

View File

@ -1,4 +1,5 @@
import Painterro from 'painterro'; import Painterro from 'painterro';
import { getThumbnailURLFromimageUrl } from './notebook-image';
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
activeColor: '#ff0000', activeColor: '#ff0000',
@ -25,11 +26,11 @@ const DEFAULT_CONFIG = {
}; };
export default class PainterroInstance { export default class PainterroInstance {
constructor(element, saveCallback) { constructor(element) {
this.elementId = element.id; this.elementId = element.id;
this.isSave = false; this.isSave = false;
this.painterroInstance = null; this.painterroInstance = undefined;
this.saveCallback = saveCallback; this.saveCallback = undefined;
} }
dismiss() { dismiss() {
@ -46,31 +47,41 @@ export default class PainterroInstance {
this.painterro = Painterro(this.config); this.painterro = Painterro(this.config);
} }
save() { save(callback) {
this.saveCallback = callback;
this.isSave = true; this.isSave = true;
this.painterroInstance.save(); this.painterroInstance.save();
} }
saveHandler(image, done) { saveHandler(image, done) {
if (this.isSave) { if (this.isSave) {
const self = this;
const url = image.asBlob(); const url = image.asBlob();
const reader = new window.FileReader(); const reader = new window.FileReader();
reader.readAsDataURL(url); reader.readAsDataURL(url);
reader.onloadend = () => { reader.onloadend = async () => {
const snapshot = reader.result; const fullSizeImageURL = reader.result;
const thumbnailURL = await getThumbnailURLFromimageUrl(fullSizeImageURL);
const snapshotObject = { const snapshotObject = {
src: snapshot, fullSizeImage: {
type: url.type, src: fullSizeImageURL,
size: url.size, type: url.type,
modified: Date.now() size: url.size,
modified: Date.now()
},
thumbnailImage: {
src: thumbnailURL,
modified: Date.now()
}
}; };
self.saveCallback(snapshotObject); this.saveCallback(snapshotObject);
};
}
done(true); done(true);
};
} else {
done(true);
}
} }
show(src) { show(src) {

View File

@ -0,0 +1,38 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import objectPathToUrl from '/src/tools/url';
export default class OpenInNewTab {
constructor(openmct) {
this.name = 'Open In New Tab';
this.key = 'newTab';
this.description = 'Open in a new browser tab';
this.group = "windowing";
this.priority = 10;
this.cssClass = "icon-new-window";
this._openmct = openmct;
}
invoke(objectPath) {
let url = objectPathToUrl(this._openmct, objectPath);
window.open(url);
}
}

View File

@ -0,0 +1,28 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import OpenInNewTabAction from './openInNewTabAction';
export default function () {
return function (openmct) {
openmct.actions.register(new OpenInNewTabAction(openmct));
};
}

View File

@ -0,0 +1,75 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
describe("the plugin", () => {
let openmct;
let openInNewTabAction;
let mockObjectPath;
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
openInNewTabAction = openmct.actions._allActions.newTab;
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('installs the open in new tab action', () => {
expect(openInNewTabAction).toBeDefined();
});
describe('when invoked', () => {
beforeEach(async () => {
mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({
identifier: {
namespace: '',
key: 'test'
}
}));
spyOnBuiltins(['open']);
await openInNewTabAction.invoke(mockObjectPath);
});
it('it opens in a new tab', () => {
expect(window.open).toHaveBeenCalled();
});
});
});

View File

@ -22,6 +22,7 @@
import CouchDocument from "./CouchDocument"; import CouchDocument from "./CouchDocument";
import CouchObjectQueue from "./CouchObjectQueue"; import CouchObjectQueue from "./CouchObjectQueue";
import { NOTEBOOK_TYPE } from '../../notebook/notebook-constants.js';
const REV = "_rev"; const REV = "_rev";
const ID = "_id"; const ID = "_id";
@ -29,24 +30,14 @@ const HEARTBEAT = 50000;
const ALL_DOCS = "_all_docs?include_docs=true"; const ALL_DOCS = "_all_docs?include_docs=true";
export default class CouchObjectProvider { export default class CouchObjectProvider {
// options {
// url: couchdb url,
// disableObserve: disable auto feed from couchdb to keep objects in sync,
// filter: selector to find objects to sync in couchdb
// }
constructor(openmct, options, namespace) { constructor(openmct, options, namespace) {
options = this._normalize(options); options = this._normalize(options);
this.openmct = openmct; this.openmct = openmct;
this.url = options.url; this.url = options.url;
this.namespace = namespace; this.namespace = namespace;
this.objectQueue = {}; this.objectQueue = {};
this.observeEnabled = options.disableObserve !== true;
this.observers = {}; this.observers = {};
this.batchIds = []; this.batchIds = [];
if (this.observeEnabled) {
this.observeObjectChanges(options.filter);
}
} }
//backwards compatibility, options used to be a url. Now it's an object //backwards compatibility, options used to be a url. Now it's an object
@ -133,8 +124,12 @@ export default class CouchObjectProvider {
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]); this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
} }
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress if (object.type === NOTEBOOK_TYPE) {
if (!this.objectQueue[key].pending) { //Temporary measure until object sync is supported for all object types
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
this.objectQueue[key].updateRevision(response[REV]);
} else if (!this.objectQueue[key].pending) {
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
this.objectQueue[key].updateRevision(response[REV]); this.objectQueue[key].updateRevision(response[REV]);
} }
@ -313,49 +308,63 @@ export default class CouchObjectProvider {
} }
observe(identifier, callback) { observe(identifier, callback) {
if (!this.observeEnabled) {
return;
}
const keyString = this.openmct.objects.makeKeyString(identifier); const keyString = this.openmct.objects.makeKeyString(identifier);
this.observers[keyString] = this.observers[keyString] || []; this.observers[keyString] = this.observers[keyString] || [];
this.observers[keyString].push(callback); this.observers[keyString].push(callback);
if (!this.isObservingObjectChanges()) {
this.observeObjectChanges();
}
return () => { return () => {
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback); this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
if (this.observers[keyString].length === 0) {
delete this.observers[keyString];
if (Object.keys(this.observers).length === 0) {
this.stopObservingObjectChanges();
}
}
}; };
} }
/** isObservingObjectChanges() {
* @private return this.stopObservingObjectChanges !== undefined;
*/
abortGetChanges() {
if (this.controller) {
this.controller.abort();
this.controller = undefined;
}
return true;
} }
/** /**
* @private * @private
*/ */
async observeObjectChanges(filter) { async observeObjectChanges() {
let intermediateResponse = this.getIntermediateResponse();
if (!this.observeEnabled) {
intermediateResponse.reject('Observe for changes is disabled');
}
const controller = new AbortController(); const controller = new AbortController();
const signal = controller.signal; const signal = controller.signal;
let filter = {selector: {}};
if (this.controller) { if (this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.length > 1) {
this.abortGetChanges(); filter.selector.$or = this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES
.map(type => {
return {
'model': {
type
}
};
});
} else {
filter.selector.model = {
type: this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES[0]
};
} }
this.controller = controller; let error = false;
if (typeof this.stopObservingObjectChanges === 'function') {
this.stopObservingObjectChanges();
}
this.stopObservingObjectChanges = () => {
controller.abort();
delete this.stopObservingObjectChanges;
};
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection // feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
// style=main_only returns only the current winning revision of the document // style=main_only returns only the current winning revision of the document
let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`; let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`;
@ -374,14 +383,20 @@ export default class CouchObjectProvider {
}, },
body body
}); });
const reader = response.body.getReader();
let completed = false;
while (!completed) { let reader;
if (response.body === undefined) {
error = true;
} else {
reader = response.body.getReader();
}
while (!error) {
const {done, value} = await reader.read(); const {done, value} = await reader.read();
//done is true when we lose connection with the provider //done is true when we lose connection with the provider
if (done) { if (done) {
completed = true; error = true;
} }
if (value) { if (value) {
@ -414,11 +429,9 @@ export default class CouchObjectProvider {
} }
//We're done receiving from the provider. No more chunks. if (error && Object.keys(this.observers).length > 0) {
intermediateResponse.resolve(true); this.observeObjectChanges();
}
return intermediateResponse.promise;
} }
/** /**

View File

@ -27,8 +27,6 @@ import {
describe('the plugin', () => { describe('the plugin', () => {
let openmct; let openmct;
let element;
let child;
let provider; let provider;
let testPath = '/test/db'; let testPath = '/test/db';
let options; let options;
@ -36,6 +34,8 @@ describe('the plugin', () => {
let mockDomainObject; let mockDomainObject;
beforeEach((done) => { beforeEach((done) => {
spyOnBuiltins(['fetch'], window);
mockDomainObject = { mockDomainObject = {
identifier: { identifier: {
namespace: '', namespace: '',
@ -51,8 +51,6 @@ describe('the plugin', () => {
}; };
openmct = createOpenMct(false); openmct = createOpenMct(false);
spyOnBuiltins(['fetch'], window);
openmct.$injector = jasmine.createSpyObj('$injector', ['get']); openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj( mockIdentifierService = jasmine.createSpyObj(
'identifierService', 'identifierService',
@ -70,10 +68,6 @@ describe('the plugin', () => {
openmct.types.addType('mock-type', {creatable: true}); openmct.types.addType('mock-type', {creatable: true});
element = document.createElement('div');
child = document.createElement('div');
element.appendChild(child);
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.startHeadless();

View File

@ -29,9 +29,10 @@ describe('the plugin', function () {
let element; let element;
let child; let child;
let openmct; let openmct;
let appHolder;
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div'); appHolder = document.createElement('div');
appHolder.style.width = '640px'; appHolder.style.width = '640px';
appHolder.style.height = '480px'; appHolder.style.height = '480px';
@ -103,7 +104,7 @@ describe('the plugin', function () {
]; ];
let planView; let planView;
beforeEach((done) => { beforeEach(() => {
planDomainObject = { planDomainObject = {
identifier: { identifier: {
key: 'test-object', key: 'test-object',
@ -140,9 +141,7 @@ describe('the plugin', function () {
let view = planView.view(planDomainObject, mockObjectPath); let view = planView.view(planDomainObject, mockObjectPath);
view.show(child, true); view.show(child, true);
return Vue.nextTick().then(() => { return Vue.nextTick();
done();
});
}); });
it('loads activities into the view', () => { it('loads activities into the view', () => {

View File

@ -74,7 +74,9 @@
</div> </div>
<div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"> <div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover">
<div class="c-button-set c-button-set--strip-h"> <div v-if="!options.compact"
class="c-button-set c-button-set--strip-h js-zoom"
>
<button class="c-button icon-minus" <button class="c-button icon-minus"
title="Zoom out" title="Zoom out"
@click="zoom('out', 0.2)" @click="zoom('out', 0.2)"
@ -86,8 +88,8 @@
> >
</button> </button>
</div> </div>
<div v-if="plotHistory.length" <div v-if="plotHistory.length && !options.compact"
class="c-button-set c-button-set--strip-h" class="c-button-set c-button-set--strip-h js-pan"
> >
<button class="c-button icon-arrow-left" <button class="c-button icon-arrow-left"
title="Restore previous pan/zoom" title="Restore previous pan/zoom"
@ -101,7 +103,7 @@
</button> </button>
</div> </div>
<div v-if="isRealTime" <div v-if="isRealTime"
class="c-button-set c-button-set--strip-h" class="c-button-set c-button-set--strip-h js-pause"
> >
<button v-if="!isFrozen" <button v-if="!isFrozen"
class="c-button icon-pause" class="c-button icon-pause"
@ -493,10 +495,12 @@ export default {
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1]; this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this); if (!this.options.compact) {
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this); this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this); this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this); this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
}
}, },
initialize() { initialize() {
@ -514,12 +518,7 @@ export default {
this.chartElementBounds = undefined; this.chartElementBounds = undefined;
this.tickUpdate = false; this.tickUpdate = false;
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1]; this.initCanvas();
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
this.config.yAxisLabel = this.config.yAxis.get('label'); this.config.yAxisLabel = this.config.yAxis.get('label');

View File

@ -1,17 +1,23 @@
<template> <template>
<div class="plot-series-limit-label" <div class="c-plot-limit"
:style="styleObj" :style="styleObj"
:class="limit.cssClass" :class="limitClass"
> >
<span class="plot-series-limit-value">{{ limit.value }}</span> <div class="c-plot-limit__label">
<span class="plot-series-color-swatch" <span class="c-plot-limit__direction-icon"></span>
:style="{ 'background-color': limit.color }" <span class="c-plot-limit__severity-icon"></span>
></span> <span class="c-plot-limit__limit-value">{{ limit.value }}</span>
<span class="plot-series-name">{{ limit.name }}</span> <span class="c-plot-limit__series-color-swatch"
:style="{ 'background-color': limit.seriesColor }"
></span>
<span class="c-plot-limit__series-name">{{ limit.name }}</span>
</div>
</div> </div>
</template> </template>
<script> <script>
import { getLimitClass } from "./limitUtil";
export default { export default {
props: { props: {
limit: { limit: {
@ -31,15 +37,14 @@ export default {
}, },
computed: { computed: {
styleObj() { styleObj() {
const top = `${this.point.top - 10}px`; const top = `${this.point.top}px`;
const left = `${this.point.left + 5}px`;
return { return {
'position': 'absolute', 'top': top
'top': top,
'left': left,
'color': '#fff'
}; };
},
limitClass() {
return getLimitClass(this.limit, 'c-plot-limit--');
} }
} }
}; };

View File

@ -1,10 +1,13 @@
<template> <template>
<hr :style="styleObj" <div :style="styleObj"
:class="cssWithoutUprLwr" class="c-plot-limit-line js-limit-line"
> :class="limitClass"
></div>
</template> </template>
<script> <script>
import { getLimitClass } from "./limitUtil";
export default { export default {
props: { props: {
point: { point: {
@ -14,30 +17,23 @@ export default {
return {}; return {};
} }
}, },
cssClass: { limit: {
type: String, type: Object,
default() { default() {
return ''; return {};
} }
} }
}, },
computed: { computed: {
styleObj() { styleObj() {
const top = `${this.point.top}px`; const top = `${this.point.top}px`;
const left = `${this.point.left}px`;
return { return {
'position': 'absolute', 'top': top
'width': '100%',
'top': top,
'left': left
}; };
}, },
cssWithoutUprLwr() { limitClass() {
let cssClass = this.cssClass.replace(/is-limit--upr/gi, 'is-limit--line'); return getLimitClass(this.limit, 'c-plot-limit-line--');
cssClass = cssClass.replace(/is-limit--lwr/gi, 'is-limit--line');
return cssClass;
} }
} }
}; };

View File

@ -32,6 +32,7 @@ export default class MCTChartAlarmLineSet {
eventHelpers.extend(this); eventHelpers.extend(this);
this.listenTo(series, 'limitBounds', this.updateBounds, this); this.listenTo(series, 'limitBounds', this.updateBounds, this);
this.listenTo(series, 'limits', this.getLimitPoints, this);
this.listenTo(series, 'change:xKey', this.getLimitPoints, this); this.listenTo(series, 'change:xKey', this.getLimitPoints, this);
if (series.limits) { if (series.limits) {
@ -69,26 +70,28 @@ export default class MCTChartAlarmLineSet {
Object.keys(series.limits).forEach((key) => { Object.keys(series.limits).forEach((key) => {
const limitForLevel = series.limits[key]; const limitForLevel = series.limits[key];
if (limitForLevel.high) { if (limitForLevel.high) {
const point = this.makePoint(Object.assign({ [xKey]: this.bounds.start }, limitForLevel.high), series);
this.limits.push({ this.limits.push({
seriesKey: series.keyString, seriesKey: series.keyString,
value: series.getYVal(limitForLevel.high), level: key.toLowerCase(),
color: this.color().asHexString(),
name: this.name(), name: this.name(),
point, seriesColor: series.get('color').asHexString(),
cssClass: limitForLevel.high.cssClass point: this.makePoint(Object.assign({ [xKey]: this.bounds.start }, limitForLevel.high), series),
value: series.getYVal(limitForLevel.high),
color: limitForLevel.high.color,
isUpper: true
}); });
} }
if (limitForLevel.low) { if (limitForLevel.low) {
const point = this.makePoint(Object.assign({ [xKey]: this.bounds.start }, limitForLevel.low), series);
this.limits.push({ this.limits.push({
seriesKey: series.keyString, seriesKey: series.keyString,
value: series.getYVal(limitForLevel.low), level: key.toLowerCase(),
color: this.color().asHexString(),
name: this.name(), name: this.name(),
point, seriesColor: series.get('color').asHexString(),
cssClass: limitForLevel.low.cssClass point: this.makePoint(Object.assign({ [xKey]: this.bounds.start }, limitForLevel.low), series),
value: series.getYVal(limitForLevel.low),
color: limitForLevel.low.color,
isUpper: false
}); });
} }
}, this); }, this);

View File

@ -46,6 +46,7 @@ import Vue from 'vue';
const MARKER_SIZE = 6.0; const MARKER_SIZE = 6.0;
const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0; const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;
const CLEARANCE = 15;
export default { export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
@ -472,13 +473,15 @@ export default {
} }
Array.from(this.$refs.limitArea.children).forEach((el) => el.remove()); Array.from(this.$refs.limitArea.children).forEach((el) => el.remove());
let limitPointOverlap = [];
this.limitLines.forEach((limitLine) => { this.limitLines.forEach((limitLine) => {
let limitContainerEl = this.$refs.limitArea; let limitContainerEl = this.$refs.limitArea;
limitLine.limits.forEach((limit) => { limitLine.limits.forEach((limit) => {
const showLabels = this.showLabels(limit.seriesKey); const showLabels = this.showLabels(limit.seriesKey);
if (showLabels) { if (showLabels) {
let limitLabelEl = this.getLimitLabel(limit); const overlap = this.getLimitOverlap(limit, limitPointOverlap);
limitPointOverlap.push(overlap);
let limitLabelEl = this.getLimitLabel(limit, overlap);
limitContainerEl.appendChild(limitLabelEl); limitContainerEl.appendChild(limitLabelEl);
} }
@ -502,14 +505,41 @@ export default {
const component = new LimitLineClass({ const component = new LimitLineClass({
propsData: { propsData: {
point, point,
cssClass: limit.cssClass limit
} }
}); });
component.$mount(); component.$mount();
return component.$el; return component.$el;
}, },
getLimitLabel(limit) { getLimitOverlap(limit, overlapMap) {
//calculate if limit lines are too close to each other
let limitTop = this.drawAPI.y(limit.point.y);
const needsVerticalAdjustment = limitTop - CLEARANCE <= 0;
let needsHorizontalAdjustment = false;
overlapMap.forEach(value => {
let diffTop;
if (limitTop > value.overlapTop) {
diffTop = limitTop - value.overlapTop;
} else {
diffTop = value.overlapTop - limitTop;
}
//need to compare +ves to +ves and -ves to -ves
if (!needsHorizontalAdjustment
&& Math.abs(diffTop) <= CLEARANCE
&& value.needsHorizontalAdjustment !== true) {
needsHorizontalAdjustment = true;
}
});
return {
needsHorizontalAdjustment,
needsVerticalAdjustment,
overlapTop: limitTop
};
},
getLimitLabel(limit, overlap) {
let point = { let point = {
left: 0, left: 0,
top: this.drawAPI.y(limit.point.y) top: this.drawAPI.y(limit.point.y)
@ -517,7 +547,7 @@ export default {
let LimitLabelClass = Vue.extend(LimitLabel); let LimitLabelClass = Vue.extend(LimitLabel);
const component = new LimitLabelClass({ const component = new LimitLabelClass({
propsData: { propsData: {
limit, limit: Object.assign({}, overlap, limit),
point point
} }
}); });

View File

@ -0,0 +1,32 @@
export function getLimitClass(limit, prefix) {
let cssClass = '';
//If color exists then use it, fall back to the cssClass
if (limit.color) {
cssClass = `${cssClass} ${prefix}${limit.color}`;
} else if (limit.cssClass) {
cssClass = `${cssClass}${limit.cssClass}`;
}
// If we applied the cssClass then skip these classes
if (limit.cssClass === undefined) {
if (limit.isUpper) {
cssClass = `${cssClass} ${prefix}upr`;
} else {
cssClass = `${cssClass} ${prefix}lwr`;
}
if (limit.level) {
cssClass = `${cssClass} ${prefix}${limit.level}`;
}
if (limit.needsHorizontalAdjustment) {
cssClass = `${cssClass} --align-label-right`;
}
if (limit.needsVerticalAdjustment) {
cssClass = `${cssClass} --align-label-below`;
}
}
return cssClass;
}

View File

@ -117,7 +117,17 @@ export default class PlotSeries extends Model {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject); this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject); this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject);
this.limits = this.limitDefinition.limits(); this.limits = [];
this.limitDefinition.limits().then(response => {
this.limits = [];
if (response) {
this.limits = response;
}
this.emit('limits', this);
});
this.openmct.time.on('bounds', this.updateLimits); this.openmct.time.on('bounds', this.updateLimits);
this.on('destroy', this.onDestroy, this); this.on('destroy', this.onDestroy, this);
} }

View File

@ -49,6 +49,8 @@
</template> </template>
<script> <script>
import {getLimitClass} from "@/plugins/plot/chart/limitUtil";
export default { export default {
props: { props: {
valueToShowWhenCollapsed: { valueToShowWhenCollapsed: {
@ -110,7 +112,7 @@ export default {
if (closest) { if (closest) {
this.formattedYValue = seriesObject.formatY(closest); this.formattedYValue = seriesObject.formatY(closest);
this.formattedXValue = seriesObject.formatX(closest); this.formattedXValue = seriesObject.formatX(closest);
this.mctLimitStateClass = closest.mctLimitState ? `${closest.mctLimitState.cssClass}` : ''; this.mctLimitStateClass = closest.mctLimitState ? getLimitClass(closest.mctLimitState, 'c-plot-limit--') : '';
} else { } else {
this.formattedYValue = ''; this.formattedYValue = '';
this.formattedXValue = ''; this.formattedXValue = '';

View File

@ -74,6 +74,8 @@
</template> </template>
<script> <script>
import {getLimitClass} from "@/plugins/plot/chart/limitUtil";
export default { export default {
props: { props: {
seriesObject: { seriesObject: {
@ -147,7 +149,7 @@ export default {
if (closest) { if (closest) {
this.formattedYValue = seriesObject.formatY(closest); this.formattedYValue = seriesObject.formatY(closest);
this.formattedXValue = seriesObject.formatX(closest); this.formattedXValue = seriesObject.formatX(closest);
this.mctLimitStateClass = seriesObject.closest.mctLimitState ? seriesObject.closest.mctLimitState.cssClass : ''; this.mctLimitStateClass = seriesObject.closest.mctLimitState ? getLimitClass(seriesObject.closest.mctLimitState, 'c-plot-limit--') : '';
} else { } else {
this.formattedYValue = ''; this.formattedYValue = '';
this.formattedXValue = ''; this.formattedXValue = '';

View File

@ -21,36 +21,36 @@
*****************************************************************************/ *****************************************************************************/
export const COLOR_PALETTE = [ export const COLOR_PALETTE = [
[0x20, 0xB2, 0xAA], [0x00, 0x37, 0xFF],
[0x9A, 0xCD, 0x32], [0xF0, 0x60, 0x00],
[0xFF, 0x8C, 0x00], [0x00, 0x70, 0x40],
[0xD2, 0xB4, 0x8C], [0xFB, 0x49, 0x49],
[0x40, 0xE0, 0xD0], [0xC8, 0x00, 0xCF],
[0x41, 0x69, 0xFF], [0x55, 0x77, 0xF2],
[0xFF, 0xD7, 0x00], [0xFF, 0xA6, 0x3D],
[0x6A, 0x5A, 0xCD], [0x05, 0xA3, 0x00],
[0xEE, 0x82, 0xEE], [0xF0, 0x00, 0x6C],
[0xCC, 0x99, 0x66], [0x77, 0x17, 0x7A],
[0x99, 0xCC, 0xCC], [0x23, 0xA9, 0xDB],
[0x66, 0xCC, 0x33], [0xFA, 0xF0, 0x6F],
[0xFF, 0xCC, 0x00], [0x4E, 0xF0, 0x48],
[0xFF, 0x66, 0x33], [0xAD, 0x50, 0x72],
[0xCC, 0x66, 0xFF], [0x94, 0x25, 0xEA],
[0xFF, 0x00, 0x66], [0x21, 0x87, 0x82],
[0xFF, 0xFF, 0x00], [0x8F, 0x6E, 0x47],
[0x80, 0x00, 0x80], [0xf0, 0x59, 0xcb],
[0x00, 0x86, 0x8B], [0x34, 0xB6, 0x7D],
[0x00, 0x8A, 0x00], [0x6A, 0x36, 0xFF],
[0xFF, 0x00, 0x00], [0x56, 0xF0, 0xE8],
[0x00, 0x00, 0xFF], [0xA1, 0x8C, 0x1C],
[0xF5, 0xDE, 0xB3], [0xCB, 0xE1, 0x44],
[0xBC, 0x8F, 0x8F], [0xFF, 0x84, 0x9E],
[0x46, 0x82, 0xB4], [0xB7, 0x79, 0xE7],
[0xFF, 0xAF, 0xAF], [0x8C, 0xC9, 0xFD],
[0x43, 0xCD, 0x80], [0xDB, 0xAA, 0x6E],
[0xCD, 0xC1, 0xC5], [0xB8, 0xDF, 0x97],
[0xA0, 0x52, 0x2D], [0xFF, 0xBC, 0xDA],
[0x64, 0x95, 0xED] [0xD3, 0xB6, 0xDE]
]; ];
export function isDefaultColor(color) { export function isDefaultColor(color) {

View File

@ -34,6 +34,7 @@ describe("the plugin", function () {
let child; let child;
let openmct; let openmct;
let telemetryPromise; let telemetryPromise;
let telemetryPromiseResolve;
let cleanupFirst; let cleanupFirst;
let mockObjectPath; let mockObjectPath;
let telemetrylimitProvider; let telemetrylimitProvider;
@ -78,7 +79,6 @@ describe("the plugin", function () {
openmct = createOpenMct(); openmct = createOpenMct();
let telemetryPromiseResolve;
telemetryPromise = new Promise((resolve) => { telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve; telemetryPromiseResolve = resolve;
}); });
@ -97,7 +97,7 @@ describe("the plugin", function () {
telemetrylimitProvider.supportsLimits.and.returnValue(true); telemetrylimitProvider.supportsLimits.and.returnValue(true);
telemetrylimitProvider.getLimits.and.returnValue({ telemetrylimitProvider.getLimits.and.returnValue({
limits: function () { limits: function () {
return { return Promise.resolve({
WARNING: { WARNING: {
low: { low: {
cssClass: "is-limit--lwr is-limit--yellow", cssClass: "is-limit--lwr is-limit--yellow",
@ -118,7 +118,7 @@ describe("the plugin", function () {
'some-key': 0.9 'some-key': 0.9
} }
} }
}; });
} }
}); });
telemetrylimitProvider.getLimitEvaluator.and.returnValue({ telemetrylimitProvider.getLimitEvaluator.and.returnValue({
@ -403,6 +403,25 @@ describe("the plugin", function () {
}); });
}); });
describe('controls in time strip view', () => {
it('zoom controls are hidden', () => {
let pauseEl = element.querySelectorAll(".c-button-set .js-zoom");
expect(pauseEl.length).toBe(0);
});
it('pan controls are hidden', () => {
let pauseEl = element.querySelectorAll(".c-button-set .js-pan");
expect(pauseEl.length).toBe(0);
});
it('pause/play controls are hidden', () => {
let pauseEl = element.querySelectorAll(".c-button-set .js-pause");
expect(pauseEl.length).toBe(0);
});
});
}); });
describe("The stacked plot view", () => { describe("The stacked plot view", () => {
@ -709,7 +728,7 @@ describe("the plugin", function () {
config.series.models[0].set('limitLines', true); config.series.models[0].set('limitLines', true);
Vue.nextTick(() => { Vue.nextTick(() => {
let limitEl = element.querySelectorAll(".js-limit-area hr"); let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
expect(limitEl.length).toBe(4); expect(limitEl.length).toBe(4);
done(); done();
}); });

View File

@ -46,6 +46,7 @@ define([
'./filters/plugin', './filters/plugin',
'./objectMigration/plugin', './objectMigration/plugin',
'./goToOriginalAction/plugin', './goToOriginalAction/plugin',
'./openInNewTabAction/plugin',
'./clearData/plugin', './clearData/plugin',
'./webPage/plugin', './webPage/plugin',
'./condition/plugin', './condition/plugin',
@ -91,6 +92,7 @@ define([
Filters, Filters,
ObjectMigration, ObjectMigration,
GoToOriginalAction, GoToOriginalAction,
OpenInNewTabAction,
ClearData, ClearData,
WebPagePlugin, WebPagePlugin,
ConditionPlugin, ConditionPlugin,
@ -190,6 +192,7 @@ define([
plugins.Filters = Filters; plugins.Filters = Filters;
plugins.ObjectMigration = ObjectMigration.default; plugins.ObjectMigration = ObjectMigration.default;
plugins.GoToOriginalAction = GoToOriginalAction.default; plugins.GoToOriginalAction = GoToOriginalAction.default;
plugins.OpenInNewTabAction = OpenInNewTabAction.default;
plugins.ClearData = ClearData; plugins.ClearData = ClearData;
plugins.WebPage = WebPagePlugin.default; plugins.WebPage = WebPagePlugin.default;
plugins.Espresso = Espresso.default; plugins.Espresso = Espresso.default;

View File

@ -19,8 +19,6 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import RemoveActionPlugin from './plugin.js';
import RemoveAction from './RemoveAction.js';
import { import {
createOpenMct, createOpenMct,
resetApplicationState, resetApplicationState,
@ -36,10 +34,6 @@ describe("The Remove Action plugin", () => {
// this setups up the app // this setups up the app
beforeEach((done) => { beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
childObject = getMockObjects({ childObject = getMockObjects({
@ -64,11 +58,10 @@ describe("The Remove Action plugin", () => {
} }
}).folder; }).folder;
// already installed by default, but never hurts, just adds to context menu
openmct.install(RemoveActionPlugin());
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(appHolder); openmct.startHeadless();
removeAction = openmct.actions._allActions.remove;
}); });
afterEach(() => { afterEach(() => {
@ -76,13 +69,12 @@ describe("The Remove Action plugin", () => {
}); });
it("should be defined", () => { it("should be defined", () => {
expect(RemoveActionPlugin).toBeDefined(); expect(removeAction).toBeDefined();
}); });
describe("when removing an object from a parent composition", () => { describe("when removing an object from a parent composition", () => {
beforeEach(() => { beforeEach(() => {
removeAction = new RemoveAction(openmct);
spyOn(removeAction, 'removeFromComposition').and.callThrough(); spyOn(removeAction, 'removeFromComposition').and.callThrough();
spyOn(removeAction, 'inNavigationPath').and.returnValue(false); spyOn(removeAction, 'inNavigationPath').and.returnValue(false);
spyOn(openmct.objects, 'mutate').and.callThrough(); spyOn(openmct.objects, 'mutate').and.callThrough();
@ -103,7 +95,6 @@ describe("The Remove Action plugin", () => {
describe("when determining the object is applicable", () => { describe("when determining the object is applicable", () => {
beforeEach(() => { beforeEach(() => {
removeAction = new RemoveAction(openmct);
spyOn(removeAction, 'appliesTo').and.callThrough(); spyOn(removeAction, 'appliesTo').and.callThrough();
}); });

View File

@ -113,7 +113,7 @@ describe('the plugin', function () {
let tabsLayoutViewProvider; let tabsLayoutViewProvider;
let mockComposition; let mockComposition;
beforeEach((done) => { beforeEach(() => {
mockComposition = new EventEmitter(); mockComposition = new EventEmitter();
mockComposition.load = () => { mockComposition.load = () => {
return Promise.resolve([telemetryItem1]); return Promise.resolve([telemetryItem1]);
@ -125,7 +125,8 @@ describe('the plugin', function () {
tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs');
let view = tabsLayoutViewProvider.view(testViewObject, []); let view = tabsLayoutViewProvider.view(testViewObject, []);
view.show(child, true); view.show(child, true);
Vue.nextTick(done);
return Vue.nextTick();
}); });
it('provides a view', () => { it('provides a view', () => {
@ -150,7 +151,7 @@ describe('the plugin', function () {
let mockComposition; let mockComposition;
let count = 0; let count = 0;
beforeEach((done) => { beforeEach(() => {
mockComposition = new EventEmitter(); mockComposition = new EventEmitter();
mockComposition.load = () => { mockComposition.load = () => {
if (count === 0) { if (count === 0) {
@ -168,7 +169,8 @@ describe('the plugin', function () {
tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs');
let view = tabsLayoutViewProvider.view(testViewObject, []); let view = tabsLayoutViewProvider.view(testViewObject, []);
view.show(child, true); view.show(child, true);
Vue.nextTick(done);
return Vue.nextTick();
}); });
it ('renders a tab for each item', () => { it ('renders a tab for each item', () => {

View File

@ -190,6 +190,11 @@
} }
} }
// All tables
td {
@include isLimit();
}
/******************************* SPECIFIC CASE WRAPPERS */ /******************************* SPECIFIC CASE WRAPPERS */
.is-editing { .is-editing {
.c-telemetry-table__headers__labels { .c-telemetry-table__headers__labels {

View File

@ -10,6 +10,7 @@
@import "../../styles/glyphs"; @import "../../styles/glyphs";
@import "../../styles/global"; @import "../../styles/global";
@import "../../styles/status"; @import "../../styles/status";
@import "../../styles/limits";
@import "../../styles/controls"; @import "../../styles/controls";
@import "../../styles/forms"; @import "../../styles/forms";
@import "../../styles/table"; @import "../../styles/table";

View File

@ -10,6 +10,7 @@
@import "../../styles/glyphs"; @import "../../styles/glyphs";
@import "../../styles/global"; @import "../../styles/global";
@import "../../styles/status"; @import "../../styles/status";
@import "../../styles/limits";
@import "../../styles/controls"; @import "../../styles/controls";
@import "../../styles/forms"; @import "../../styles/forms";
@import "../../styles/table"; @import "../../styles/table";

View File

@ -10,6 +10,7 @@
@import "../../styles/glyphs"; @import "../../styles/glyphs";
@import "../../styles/global"; @import "../../styles/global";
@import "../../styles/status"; @import "../../styles/status";
@import "../../styles/limits";
@import "../../styles/controls"; @import "../../styles/controls";
@import "../../styles/forms"; @import "../../styles/forms";
@import "../../styles/table"; @import "../../styles/table";

View File

@ -50,9 +50,6 @@ describe('the plugin', function () {
} }
} }
]; ];
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
openmct.install(new TimelinePlugin()); openmct.install(new TimelinePlugin());
@ -73,7 +70,7 @@ describe('the plugin', function () {
}); });
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(appHolder); openmct.startHeadless();
}); });
afterEach(() => { afterEach(() => {
@ -100,7 +97,7 @@ describe('the plugin', function () {
describe('the view', () => { describe('the view', () => {
let timelineView; let timelineView;
beforeEach((done) => { beforeEach(() => {
const testViewObject = { const testViewObject = {
id: "test-object", id: "test-object",
type: "time-strip" type: "time-strip"
@ -110,7 +107,8 @@ describe('the plugin', function () {
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
let view = timelineView.view(testViewObject, element); let view = timelineView.view(testViewObject, element);
view.show(child, true); view.show(child, true);
Vue.nextTick(done);
return Vue.nextTick();
}); });
it('provides a view', () => { it('provides a view', () => {

View File

@ -314,12 +314,21 @@ $colorTelemStale: pushBack($colorBodyFg, 20%);
$styleTelemStale: italic; $styleTelemStale: italic;
// Limits // Limits
$colorLimitYellowBg: #ac7300; $colorLimitYellowBg: #B18B05;
$colorLimitYellowFg: #ffe64d; $colorLimitYellowFg: #FEEEB5;
$colorLimitYellowIc: #ffb607; $colorLimitYellowIc: #FDC707;
$colorLimitOrangeBg: #B36B00;
$colorLimitOrangeFg: #FFE0B2;
$colorLimitOrangeIc: #ff9900;
$colorLimitRedBg: #940000; $colorLimitRedBg: #940000;
$colorLimitRedFg: #ffa489; $colorLimitRedFg: #ffa489;
$colorLimitRedIc: #ff4222; $colorLimitRedIc: #ff4222;
$colorLimitPurpleBg: #891BB3;
$colorLimitPurpleFg: #EDBEFF;
$colorLimitPurpleIc: #c327ff;
$colorLimitCyanBg: #4BA6B3;
$colorLimitCyanFg: #D3FAFF;
$colorLimitCyanIc: #6BEDFF;
// Bubble colors // Bubble colors
$colorInfoBubbleBg: #dddddd; $colorInfoBubbleBg: #dddddd;
@ -359,6 +368,7 @@ $colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: pushBack($colorPlotFg, 20%); $colorPlotLabelFg: pushBack($colorPlotFg, 20%);
$legendHoverValueBg: rgba($colorBodyFg, 0.2); $legendHoverValueBg: rgba($colorBodyFg, 0.2);
$legendTableHeadBg: $colorTabHeaderBg; $legendTableHeadBg: $colorTabHeaderBg;
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);
// Tree // Tree
$colorTreeBg: transparent; $colorTreeBg: transparent;

View File

@ -318,12 +318,21 @@ $colorTelemStale: pushBack($colorBodyFg, 20%);
$styleTelemStale: italic; $styleTelemStale: italic;
// Limits // Limits
$colorLimitYellowBg: #ac7300; $colorLimitYellowBg: #B18B05;
$colorLimitYellowFg: #ffe64d; $colorLimitYellowFg: #FEEEB5;
$colorLimitYellowIc: #ffb607; $colorLimitYellowIc: #FDC707;
$colorLimitOrangeBg: #B36B00;
$colorLimitOrangeFg: #FFE0B2;
$colorLimitOrangeIc: #ff9900;
$colorLimitRedBg: #940000; $colorLimitRedBg: #940000;
$colorLimitRedFg: #ffa489; $colorLimitRedFg: #ffa489;
$colorLimitRedIc: #ff4222; $colorLimitRedIc: #ff4222;
$colorLimitPurpleBg: #891BB3;
$colorLimitPurpleFg: #EDBEFF;
$colorLimitPurpleIc: #c327ff;
$colorLimitCyanBg: #4BA6B3;
$colorLimitCyanFg: #D3FAFF;
$colorLimitCyanIc: #6BEDFF;
// Bubble colors // Bubble colors
$colorInfoBubbleBg: #dddddd; $colorInfoBubbleBg: #dddddd;
@ -363,6 +372,7 @@ $colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: pushBack($colorPlotFg, 20%); $colorPlotLabelFg: pushBack($colorPlotFg, 20%);
$legendHoverValueBg: rgba($colorBodyFg, 0.2); $legendHoverValueBg: rgba($colorBodyFg, 0.2);
$legendTableHeadBg: rgba($colorBodyFg, 0.15); $legendTableHeadBg: rgba($colorBodyFg, 0.15);
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);
// Tree // Tree
$colorTreeBg: transparent; $colorTreeBg: transparent;

View File

@ -317,9 +317,18 @@ $styleTelemStale: italic;
$colorLimitYellowBg: #ffe64d; $colorLimitYellowBg: #ffe64d;
$colorLimitYellowFg: #7f4f20; $colorLimitYellowFg: #7f4f20;
$colorLimitYellowIc: #e7a115; $colorLimitYellowIc: #e7a115;
$colorLimitOrangeBg: #B36B00;
$colorLimitOrangeFg: #FFE0B2;
$colorLimitOrangeIc: #ff9900;
$colorLimitRedBg: #ff0000; $colorLimitRedBg: #ff0000;
$colorLimitRedFg: #fff; $colorLimitRedFg: #fff;
$colorLimitRedIc: #ffa99a; $colorLimitRedIc: #ffa99a;
$colorLimitPurpleBg: #891BB3;
$colorLimitPurpleFg: #EDBEFF;
$colorLimitPurpleIc: #c327ff;
$colorLimitCyanBg: #4BA6B3;
$colorLimitCyanFg: #D3FAFF;
$colorLimitCyanIc: #1795c0;
// Bubble colors // Bubble colors
$colorInfoBubbleBg: $colorMenuBg; $colorInfoBubbleBg: $colorMenuBg;
@ -359,6 +368,7 @@ $colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: pushBack($colorPlotFg, 20%); $colorPlotLabelFg: pushBack($colorPlotFg, 20%);
$legendHoverValueBg: rgba($colorBodyFg, 0.2); $legendHoverValueBg: rgba($colorBodyFg, 0.2);
$legendTableHeadBg: rgba($colorBodyFg, 0.15); $legendTableHeadBg: rgba($colorBodyFg, 0.15);
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.4);
// Tree // Tree
$colorTreeBg: transparent; $colorTreeBg: transparent;

View File

@ -136,6 +136,7 @@ mct-plot {
left: nth($plotDisplayArea, 4); left: nth($plotDisplayArea, 4);
.gl-plot-display-area { .gl-plot-display-area {
overflow: hidden;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
@ -538,12 +539,9 @@ mct-plot {
} }
.plot-series-color-swatch { .plot-series-color-swatch {
border-radius: 30%; //$smallCr; @include colorSwatch();
border: 1px solid $colorBodyBg;
display: inline-block; display: inline-block;
flex: 0 0 auto; flex: 0 0 auto;
height: $plotSwatchD;
width: $plotSwatchD;
} }
.plot-series-name { .plot-series-name {
display: inline; display: inline;
@ -552,6 +550,7 @@ mct-plot {
.plot-series-value { .plot-series-value {
@include ellipsize(); @include ellipsize();
@include isLimit();
} }
} }

270
src/styles/_limits.scss Normal file
View File

@ -0,0 +1,270 @@
$plotLimitLineSize: 1px;
$plotLimitDashWidthOffset: 10px;
$lineBlocker: $colorPlotLimitLineBg;
$plotLimitDashWidthSeverity: 50px;
$plotLimitDashWidthCritical: $plotLimitDashWidthSeverity - $plotLimitDashWidthOffset;
$plotLimitDashWidthDistress: $plotLimitDashWidthCritical - $plotLimitDashWidthOffset;
$plotLimitDashWidthWarning: $plotLimitDashWidthDistress - $plotLimitDashWidthOffset;
$plotLimitDashWidthWatch: $plotLimitDashWidthWarning - $plotLimitDashWidthOffset;
@mixin plotLimitLine($c, $breakPerc) {
background: $lineBlocker linear-gradient(
90deg,
$c $breakPerc,
transparent $breakPerc,
transparent 100%
) repeat-x;
}
@mixin plotLimitDirectionGradient($c, $deg: 0deg) {
background: linear-gradient(
$deg,
$c,
transparent
)
}
@mixin plotLimitLineUpper($c) {
$breakPerc: 80%;
@include plotLimitLine($c: $c, $breakPerc: $breakPerc);
}
@mixin plotLimitLineLower($c) {
$breakPerc: 30%;
@include plotLimitLine($c: $c, $breakPerc: $breakPerc);
}
.c-plot-limit-line {
box-shadow: $lineBlocker 0 0 0 2px;
height: $plotLimitLineSize;
width: 100%;
position: absolute;
z-index: 1;
// Colors and directions
&--purple.c-plot-limit-line--upr {
@include plotLimitLineUpper($colorLimitPurpleIc);
}
&--purple.c-plot-limit-line--lwr {
@include plotLimitLineLower($colorLimitPurpleIc);
}
&--red.c-plot-limit-line--upr {
@include plotLimitLineUpper($colorLimitRedIc);
}
&--red.c-plot-limit-line--lwr {
@include plotLimitLineLower($colorLimitRedIc);
}
&--orange.c-plot-limit-line--upr {
@include plotLimitLineUpper($colorLimitOrangeIc);
}
&--orange.c-plot-limit-line--lwr {
@include plotLimitLineLower($colorLimitOrangeIc);
}
&--yellow.c-plot-limit-line--upr {
@include plotLimitLineUpper($colorLimitYellowIc);
}
&--yellow.c-plot-limit-line--lwr {
@include plotLimitLineLower($colorLimitYellowIc);
}
&--cyan.c-plot-limit-line--upr {
@include plotLimitLineUpper($colorLimitCyanIc);
}
&--cyan.c-plot-limit-line--lwr {
@include plotLimitLineLower($colorLimitCyanIc);
}
// Severities
&--severe {
background-size: $plotLimitDashWidthSeverity 100% !important;
}
&--critical {
background-size: $plotLimitDashWidthCritical 100% !important;
}
&--distress {
background-size: $plotLimitDashWidthDistress 100% !important;
}
&--warning {
background-size: $plotLimitDashWidthWarning 100% !important;
}
&--watch {
background-size: $plotLimitDashWidthWatch 100% !important;
}
}
.c-plot-limit {
// Holds both label and directional gradient
$labelCr: $basicCr;
display: flex;
position: absolute;
width: 100%;
z-index: 0;
&__label {
border-width: 1px 1px 0 0;
border-style: solid;
border-radius: 0 $labelCr 0 0;
display: flex;
flex: 0 0 auto;
align-items: center;
padding: 2px 4px;
transform: translateY(-100%);
> * + * {
margin-left: $interiorMarginSm;
}
}
&.--align-label-right {
justify-content: flex-end;
.c-plot-limit__label {
border-radius: $labelCr 0 0 0;
border-width: 1px 0 0 1px;
}
}
&.--align-label-below {
.c-plot-limit__label {
border-radius: 0 0 $labelCr 0;
border-width: 0 1px 1px 0;
transform: translateY(0);
}
&.--align-label-right {
.c-plot-limit__label {
border-radius: 0 0 0 $labelCr;
border-width: 0 0 1px 1px;
}
}
}
[class*='icon'] {
&:before {
display: block;
font-family: symbolsfont;
font-size: 0.9em;
}
}
&__series-color-swatch {
@include colorSwatch();
display: block;
flex: 0 0 auto;
}
&:before {
// Direction gradient
content: "";
display: block;
position: absolute;
left: 0;
right: 0;
height: 100%;
opacity: 0.2;
}
&--upr:before {
transform: translateY(-100%);
}
&--lwr:before {
transform: scaleY(-1); // This inverts the gradient direction
}
// Label styling
&--purple [class*='label'] {
background-color: $colorLimitPurpleBg;
border-color: $colorLimitPurpleIc;
color: $colorLimitPurpleFg;
}
&--red [class*='label'] {
background-color: $colorLimitRedBg;
border-color: $colorLimitRedIc;
color: $colorLimitRedFg;
}
&--orange [class*='label'] {
background-color: $colorLimitOrangeBg;
border-color: $colorLimitOrangeIc;
color: $colorLimitOrangeFg;
}
&--yellow [class*='label'] {
background-color: $colorLimitYellowBg;
border-color: $colorLimitYellowIc;
color: $colorLimitYellowFg;
}
&--cyan [class*='label'] {
background-color: $colorLimitCyanBg;
border-color: $colorLimitCyanIc;
color: $colorLimitCyanFg;
}
// Directional gradients
&--purple:before {
@include plotLimitDirectionGradient($c: $colorLimitPurpleIc);
}
&--red:before {
@include plotLimitDirectionGradient($c: $colorLimitRedIc);
}
&--orange:before {
@include plotLimitDirectionGradient($c: $colorLimitOrangeIc);
}
&--yellow:before {
@include plotLimitDirectionGradient($c: $colorLimitYellowIc);
}
&--cyan:before {
@include plotLimitDirectionGradient($c: $colorLimitCyanIc);
}
}
// Severity icons
.c-plot-limit__label .c-plot-limit__severity-icon:before {
.c-plot-limit--severe & {
content: $glyph-icon-alert-triangle;
}
.c-plot-limit--critical & {
content: $glyph-icon-alert-rect;
}
.c-plot-limit--distress & {
content: $glyph-icon-bell;
}
.c-plot-limit--warning & {
content: $glyph-icon-asterisk;
}
.c-plot-limit--watch & {
content: $glyph-icon-eye-open;
}
}
// Direction icons
.c-plot-limit__label .c-plot-limit__direction-icon:before {
.c-plot-limit--upr & {
content: $glyph-icon-arrow-up;
}
.c-plot-limit--lwr & {
content: $glyph-icon-arrow-down;
}
}

View File

@ -151,6 +151,25 @@
} }
} }
@mixin isLimit() {
&[class*='is-limit'] {
&:before {
display: inline-block;
font-family: symbolsfont;
margin-right: $interiorMarginSm;
}
}
&.is-limit--lwr:before { content: $glyph-icon-arrow-down; }
&.is-limit--upr:before { content: $glyph-icon-arrow-up; }
&.is-limit--purple { background: $colorLimitPurpleBg !important; color: $colorLimitPurpleFg !important; }
&.is-limit--red { background: $colorLimitRedBg !important; color: $colorLimitRedFg !important; }
&.is-limit--orange { background: $colorLimitOrangeBg !important; color: $colorLimitOrangeFg !important; }
&.is-limit--yellow { background: $colorLimitYellowBg !important; color: $colorLimitYellowFg !important; }
&.is-limit--cyan { background: $colorLimitCyanBg !important; color: $colorLimitCyanFg !important; }
}
@mixin bgDiagonalStripes($c: yellow, $a: 0.1, $d: 40px) { @mixin bgDiagonalStripes($c: yellow, $a: 0.1, $d: 40px) {
background-image: linear-gradient(-45deg, background-image: linear-gradient(-45deg,
rgba($c, $a) 25%, transparent 25%, rgba($c, $a) 25%, transparent 25%,
@ -222,6 +241,13 @@
background-size: $bgSize; background-size: $bgSize;
} }
@mixin colorSwatch() {
border-radius: 30%;
border: 1px solid $colorBodyBg;
height: $plotSwatchD;
width: $plotSwatchD;
}
@mixin noColor() { @mixin noColor() {
// A "no fill/stroke" selection option. Used in palettes. // A "no fill/stroke" selection option. Used in palettes.
$c: red; $c: red;

View File

@ -65,15 +65,6 @@
} }
} }
@mixin andUprLwr {
&.is-limit--upr:before { content: $glyph-icon-arrow-up !important; }
&.is-limit--lwr:before { content: $glyph-icon-arrow-down !important; }
}
@mixin andLine {
&.is-limit--line:before { content: '' !important; }
}
@mixin uIndicator($bg, $fg, $glyph) { @mixin uIndicator($bg, $fg, $glyph) {
background: $bg; background: $bg;
color: $fg; color: $fg;
@ -95,26 +86,9 @@
display: inline-block; display: inline-block;
padding: 2px $interiorMargin; padding: 2px $interiorMargin;
} }
} }
/*************************************************** STYLES */ /*************************************************** STYLES */
*:not(tr) {
&.is-limit--yellow {
@include statusStyle($colorLimitYellowBg, $colorLimitYellowFg, true);
@include statusIcon($colorLimitYellowIc, $glyph-icon-alert-rect);
@include andUprLwr();
@include andLine();
}
&.is-limit--red {
@include statusStyle($colorLimitRedBg, $colorLimitRedFg, true);
@include statusIcon($colorLimitRedIc, $glyph-icon-alert-triangle);
@include andUprLwr();
@include andLine();
}
}
tr { tr {
&.is-limit--yellow { &.is-limit--yellow {
@include statusStyle($colorLimitYellowBg, $colorLimitYellowFg); @include statusStyle($colorLimitYellowBg, $colorLimitYellowFg);

View File

@ -21,38 +21,39 @@
*****************************************************************************/ *****************************************************************************/
/** /**
* Module defining NewTabAction (Originally NewWindowAction). Created by vwoeltje on 11/18/14. * Module defining url handling.
*/ */
define(
[],
function () {
/**
* The new tab action allows a domain object to be opened
* into a new browser tab.
* @memberof platform/commonUI/browse
* @constructor
* @implements {Action}
*/
function NewTabAction(urlService, $window, context) {
context = context || {};
this.urlService = urlService; export function paramsToArray(openmct) {
this.open = function () { // parse urParams from an object to an array.
arguments[0] += "&hideTree=true&hideInspector=true"; let urlParams = openmct.router.getParams();
$window.open.apply($window, arguments); let newTabParams = [];
}; for (let key in urlParams) {
if ({}.hasOwnProperty.call(urlParams, key)) {
// Choose the object to be opened into a new tab let param = `${key}=${urlParams[key]}`;
this.domainObject = context.selectedObject || context.domainObject; newTabParams.push(param);
} }
NewTabAction.prototype.perform = function () {
this.open(
this.urlService.urlForNewTab("browse", this.domainObject),
"_blank"
);
};
return NewTabAction;
} }
);
return newTabParams;
}
export function identifierToString(openmct, objectPath) {
let identifier = '#/browse/' + objectPath.map(function (o) {
return o && openmct.objects.makeKeyString(o.identifier);
})
.reverse()
.join('/');
return identifier;
}
export default function objectPathToUrl(openmct, objectPath) {
let url = identifierToString(openmct, objectPath);
let urlParams = paramsToArray(openmct);
if (urlParams.length) {
url += '?' + urlParams.join('&');
}
return url;
}

View File

@ -118,10 +118,11 @@ export default {
this.openmct.objectViews.off('clearData', this.clearData); this.openmct.objectViews.off('clearData', this.clearData);
}, },
getStyleReceiver() { getStyleReceiver() {
let styleReceiver = this.$el.querySelector('.js-style-receiver'); let styleReceiver = this.$el.querySelector('.js-style-receiver')
|| this.$el.querySelector(':first-child');
if (!styleReceiver) { if (styleReceiver === null) {
styleReceiver = this.$el.querySelector(':first-child'); styleReceiver = undefined;
} }
return styleReceiver; return styleReceiver;
@ -142,12 +143,13 @@ export default {
this.updateView(true); this.updateView(true);
}, },
updateStyle(styleObj) { updateStyle(styleObj) {
if (!styleObj) { let elemToStyle = this.getStyleReceiver();
if (!styleObj || elemToStyle === undefined) {
return; return;
} }
let keys = Object.keys(styleObj); let keys = Object.keys(styleObj);
let elemToStyle = this.getStyleReceiver();
keys.forEach(key => { keys.forEach(key => {
if (elemToStyle) { if (elemToStyle) {
@ -373,11 +375,17 @@ export default {
}, },
setFontSize(newSize) { setFontSize(newSize) {
let elemToStyle = this.getStyleReceiver(); let elemToStyle = this.getStyleReceiver();
elemToStyle.dataset.fontSize = newSize;
if (elemToStyle !== undefined) {
elemToStyle.dataset.fontSize = newSize;
}
}, },
setFont(newFont) { setFont(newFont) {
let elemToStyle = this.getStyleReceiver(); let elemToStyle = this.getStyleReceiver();
elemToStyle.dataset.font = newFont;
if (elemToStyle !== undefined) {
elemToStyle.dataset.font = newFont;
}
} }
} }
}; };

View File

@ -0,0 +1,218 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import {
mockLocalStorage
} from 'utils/testing/mockLocalStorage';
import {
mockTelemetryTableSelection,
mockMultiSelectionSameStyles,
mockMultiSelectionMixedStyles,
mockMultiSelectionNonSpecificStyles,
mockStyle
} from './InspectorStylesSpecMocks';
import Vue from 'vue';
import StylesView from '@/plugins/condition/components/inspector/StylesView.vue';
import SavedStylesView from '@/ui/inspector/styles/SavedStylesView.vue';
import stylesManager from '@/ui/inspector/styles/StylesManager';
describe("the inspector", () => {
let openmct;
let selection;
let stylesViewComponent;
let savedStylesViewComponent;
mockLocalStorage();
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it("should allow a style to be saved", () => {
selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(0);
stylesViewComponent.$children[0].saveStyle(mockStyle);
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(1);
});
it("should display all saved styles", () => {
selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
expect(savedStylesViewComponent.$children[0].$children.length).toBe(0);
stylesViewComponent.$children[0].saveStyle(mockStyle);
stylesViewComponent.$nextTick().then(() => {
expect(savedStylesViewComponent.$children[0].$children.length).toBe(1);
});
});
it("should allow a saved style to be applied", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$children[0].saveStyle(mockStyle);
stylesViewComponent.$nextTick().then(() => {
const styleSelectorComponent = savedStylesViewComponent.$children[0].$children[0];
styleSelectorComponent.selectStyle();
savedStylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const styles = styleEditorComponent.$children.filter(component => component.options.value === mockStyle.color);
expect(styles.length).toBe(3);
});
});
});
it("should allow a saved style to be deleted", () => {
selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$children[0].saveStyle(mockStyle);
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(1);
savedStylesViewComponent.$children[0].deleteStyle(0);
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(0);
});
it("should prevent a style from being saved when the number of saved styles is at the limit", () => {
spyOn(SavedStylesView.methods, 'showLimitReachedDialog').and.callThrough();
selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
for (let i = 1; i <= 20; i++) {
stylesViewComponent.$children[0].saveStyle(mockStyle);
}
expect(SavedStylesView.methods.showLimitReachedDialog).not.toHaveBeenCalled();
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(20);
stylesViewComponent.$children[0].saveStyle(mockStyle);
expect(SavedStylesView.methods.showLimitReachedDialog).toHaveBeenCalled();
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(20);
});
it("should allow styles from multi-selections to be saved", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockMultiSelectionSameStyles;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;
const saveStyleButton = styleEditorComponent.$children[saveStyleButtonIndex];
expect(saveStyleButton.$listeners.click).not.toBe(undefined);
saveStyleButton.$listeners.click();
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(1);
});
});
it("should prevent mixed styles from being saved", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockMultiSelectionMixedStyles;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;
const saveStyleButton = styleEditorComponent.$children[saveStyleButtonIndex];
expect(saveStyleButton.$listeners.click).toBe(undefined);
});
});
it("should prevent non-specific styles from being saved", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockMultiSelectionNonSpecificStyles;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;
const saveStyleButton = styleEditorComponent.$children[saveStyleButtonIndex];
expect(saveStyleButton.$listeners.click).toBe(undefined);
});
});
function createViewComponent(component) {
const element = document.createElement('div');
const child = document.createElement('div');
element.appendChild(child);
const config = {
provide: {
openmct,
selection,
stylesManager
},
el: element,
components: {},
template: `<${component.name} />`
};
config.components[component.name] = component;
return new Vue(config).$mount();
}
});

View File

@ -0,0 +1,204 @@
export const mockTelemetryTableSelection = [
[{
context: {
item: {
configuration: {},
type: 'table',
identifier: {
key: 'mock-telemetry-table-1',
namespace: ''
}
}
}
}]
];
export const mockStyle = {
backgroundColor: '#ff0000',
border: '#ff0000',
color: '#ff0000'
};
const mockDisplayLayoutPath = {
context: {
item: {
identifier: {
key: "6af3200d-928b-4ff0-8ed0-b94a0e6752d1",
namespace: ""
},
type: "layout",
configuration: {
items: [
{
id: "dd3202e5-40d0-4112-8951-00f0f1ed6a29",
type: "text-view",
fontSize: "default",
font: "default"
},
{
id: "b522d636-90b2-4f5f-9588-2a0345c30f87",
type: "text-view",
fontSize: "default",
font: "default"
},
{
id: "537b7596-b442-44fe-b464-07f56bdc67c8",
type: "text-view",
fontSize: "default",
font: "default"
},
{
id: "3f17162f-a822-4e39-8332-6aa39b79d022",
type: "text-view",
fontSize: "default",
font: "default"
},
{
id: "c1c5acd8-a14b-450c-8c94-ce0075dd9912",
type: "text-view",
fontSize: "8",
font: "monospace-bold"
}
],
objectStyles: {
"dd3202e5-40d0-4112-8951-00f0f1ed6a29": {
staticStyle: {
style: {
backgroundColor: "#0000ff",
border: "1px solid #0000ff"
}
}
},
"b522d636-90b2-4f5f-9588-2a0345c30f87": {
staticStyle: {
style: {
backgroundColor: "#ff0000",
border: "1px solid #ff0000"
}
}
},
"537b7596-b442-44fe-b464-07f56bdc67c8": {
staticStyle: {
style: {
backgroundColor: "#ff0000",
border: "1px solid #ff0000"
}
}
},
"3f17162f-a822-4e39-8332-6aa39b79d022": {
staticStyle: {
style: {
backgroundColor: "#0000ff",
border: "1px solid #0000ff",
color: "#0000ff"
}
}
},
"c1c5acd8-a14b-450c-8c94-ce0075dd9912": {
staticStyle: {
style: {
backgroundColor: "#0000ff",
border: "1px solid #0000ff",
color: "#0000ff"
}
}
}
}
}
},
supportsMultiSelect: true
}
};
const mockTextBox1Path = {
context: {
index: 0,
layoutItem: {
id: "dd3202e5-40d0-4112-8951-00f0f1ed6a29",
type: "text-view",
fontSize: "default",
font: "default"
}
}
};
const mockTextBox2Path = {
context: {
index: 1,
layoutItem: {
id: "b522d636-90b2-4f5f-9588-2a0345c30f87",
type: "text-view",
fontSize: "default",
font: "default"
}
}
};
const mockTextBox3Path = {
context: {
index: 2,
layoutItem: {
id: "537b7596-b442-44fe-b464-07f56bdc67c8",
type: "text-view",
fontSize: "default",
font: "default"
}
}
};
const mockTextBox4Path = {
context: {
index: 3,
layoutItem: {
id: "3f17162f-a822-4e39-8332-6aa39b79d022",
type: "text-view",
fontSize: "default",
font: "default"
}
}
};
const mockTextBox5Path = {
context: {
index: 4,
layoutItem: {
id: "c1c5acd8-a14b-450c-8c94-ce0075dd9912",
type: "text-view",
fontSize: "8",
font: "default-bold"
}
}
};
export const mockMultiSelectionSameStyles = [
[
mockTextBox2Path,
mockDisplayLayoutPath
],
[
mockTextBox3Path,
mockDisplayLayoutPath
]
];
export const mockMultiSelectionMixedStyles = [
[
mockTextBox1Path,
mockDisplayLayoutPath
],
[
mockTextBox2Path,
mockDisplayLayoutPath
]
];
export const mockMultiSelectionNonSpecificStyles = [
[
mockTextBox4Path,
mockDisplayLayoutPath
],
[
mockTextBox5Path,
mockDisplayLayoutPath
]
];

View File

@ -308,6 +308,7 @@ export default {
|| !checkItem.navigationPath.includes(path); || !checkItem.navigationPath.includes(path);
}); });
this.openTreeItems.splice(pathIndex, 1); this.openTreeItems.splice(pathIndex, 1);
this.removeCompositionListenerFor(path);
}, },
closeTreeItem(item) { closeTreeItem(item) {
this.closeTreeItemByPath(item.navigationPath); this.closeTreeItemByPath(item.navigationPath);
@ -393,12 +394,18 @@ export default {
} }
const indexOfScroll = this.treeItems.findIndex(item => item.navigationPath === navigationPath); const indexOfScroll = this.treeItems.findIndex(item => item.navigationPath === navigationPath);
const scrollTopAmount = indexOfScroll * this.itemHeight;
this.$refs.scrollable.scrollTo({ if (indexOfScroll !== -1) {
top: scrollTopAmount, const scrollTopAmount = indexOfScroll * this.itemHeight;
behavior: 'smooth'
}); this.$refs.scrollable.scrollTo({
top: scrollTopAmount,
behavior: 'smooth'
});
} else if (this.scrollToPath) {
this.scrollToPath = undefined;
delete this.scrollToPath;
}
}, },
scrollEndEvent() { scrollEndEvent() {
this.$nextTick(() => { this.$nextTick(() => {

View File

@ -1,3 +1,5 @@
import objectPathToUrl from '/src/tools/url';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
props: { props: {
@ -18,10 +20,9 @@ export default {
return '#' + this.navigateToPath; return '#' + this.navigateToPath;
} }
return '#/browse/' + this.objectPath let url = objectPathToUrl(this.openmct, this.objectPath);
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse() return url;
.join('/');
} }
} }
}; };

View File

@ -21,6 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import MCT from 'MCT'; import MCT from 'MCT';
let nativeFunctions = []; let nativeFunctions = [];
let mockObjects = setMockObjects(); let mockObjects = setMockObjects();

View File

@ -0,0 +1,33 @@
export function mockLocalStorage() {
let store;
beforeEach(() => {
spyOn(Storage.prototype, 'getItem').and.callFake(getItem);
spyOn(Storage.prototype, 'setItem').and.callFake(setItem);
spyOn(Storage.prototype, 'removeItem').and.callFake(removeItem);
spyOn(Storage.prototype, 'clear').and.callFake(clear);
store = {};
function getItem(key) {
return store[key];
}
function setItem(key, value) {
store[key] = typeof value === 'string' ? value : JSON.stringify(value);
}
function removeItem(key) {
store[key] = undefined;
delete store[key];
}
function clear() {
store = {};
}
});
afterEach(() => {
store = undefined;
});
}