Compare commits

...

40 Commits

Author SHA1 Message Date
Andrew Henry
40e05e636b Merge branch 'master' into view-large-snapshot 2021-09-13 17:31:30 -07:00
Nikhil
d7c9c9cb98 Snapshot images should use the namespace of the notebook they are being saved to or LocalStorage (#4020)
* Snapshot images should use the namespace of the notebook they are being saved to, or LocalStorage #4007

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-09-13 13:44:38 -07:00
Nikhil
63fb10a9a9 Merge branch 'master' into view-large-snapshot 2021-09-13 12:03:50 -07:00
Scott Bell
2131ef2397 Clear data for objects clears data for all objects
Clear data for objects clears data for all objects #4065
2021-09-10 11:30:54 -07:00
Andrew Henry
48c22369a1 Fixed memory leak in import-export plugin (#4061) 2021-09-02 15:45:11 -07:00
David Tsay
6506077f4d refactor panes (#4125)
* remove router listeners
* remove some hard-coding
2021-09-02 14:11:05 -07:00
Nikhil Mandlik
6af5801b1a when in edit mode, disable navigation link to notebook entry. 2021-09-01 12:57:10 -07:00
Nikhil Mandlik
ffe9f04ac6 corrected variable name from 'getRelativePath' to 'relativePath' 2021-09-01 12:57:10 -07:00
Nikhil Mandlik
df28fe47cc addressed review comments. 2021-09-01 12:57:10 -07:00
Nikhil Mandlik
b2dd51b30d added disableAutoHide flag to specify active overlays to exclude from list to hide. 2021-09-01 12:57:10 -07:00
Nikhil Mandlik
248e8c9caf added skipHidePreviousOverlay flag to snapshoting overlay work. 2021-09-01 12:57:10 -07:00
Nikhil Mandlik
32cd981e74 use preview component inside viewLargeAction 2021-09-01 12:57:10 -07:00
Nikhil Mandlik
9e9e4bff67 added NotebookMenuSwitcher in preview header 2021-09-01 12:57:08 -07:00
Nikhil Mandlik
34597c2a5d refactor notebook snapshot menu into new file 2021-09-01 12:45:59 -07:00
Nikhil Mandlik
b7f8060523 handle snapshot link navigation bug while edit mode is on 2021-09-01 12:43:17 -07:00
Nikhil Mandlik
20b6fb3014 fixed preview view menu error. 2021-09-01 12:43:17 -07:00
John Hill
b1b4266ff3 Add automated security scanning to our repo (#4166) 2021-08-30 15:44:25 -07:00
Charles Hacskaylo
42b0148f93 Add new icon for aggregate telemetry (#4163) 2021-08-30 11:19:47 -07:00
Charles Hacskaylo
9461ad8edd Add new Ellipse drawing object in Display Layouts (#4153)
* Add new ellipse drawing object to Display Layouts
- Fix test and linting issues;

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-08-30 13:29:05 -04:00
Shefali Joshi
40055ba955 Prepare for sprint 1.7.8 (#4156) 2021-08-27 04:55:30 -07:00
Shefali Joshi
9cb85ad176 1.7.7 merge into master (#4155)
* Merge 1.7.7 sprint branch into master

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
2021-08-27 04:44:23 -07:00
Khalid Adil
f2b2953a5d [Display Layout] Right click on item acts as sticky move (#4126)
* Added a left click filter to the mousedown events
* Moved ternary logic out of the template
* Move defauts into function params
2021-08-23 16:15:57 -07:00
arfy slowy
62de310686 fix: typo spelling grammar (#4095)
* fix: typo spelling grammar
2021-08-23 15:43:58 -07:00
Nikhil
4b9ff67e49 [Notebook] Create Snapshot directly from any frame in a layout #3300 (#4099) 2021-08-23 14:03:49 -07:00
Jamie V
d5e32ec494 [Telemetry Collection] Fixes: abort requests, naming, bindings, errors, time system handling (#4128)
* naming signal correctly for aborint
* updating api call for requesting telemetry collections

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-08-19 17:21:35 -07:00
John Hill
38880ba3d1 Update job names to be more explicit (#4137)
It's still not clear which jobs pass/fail on tests. This change will update the name of the job which is run to be more explicit that it only pass/fails on the build step
2021-08-19 13:25:07 -07:00
Jamie V
a99ce7733c changed from window resize event, to element ResizeObserver for more accurate tracking of tree size (#4132) 2021-08-18 16:38:48 -07:00
John Hill
9f48764210 Add Nightly CI Job to Circle to identify flake on master (#4032)
* Nightly job
* Unique job names
2021-08-18 13:16:15 -07:00
Jon Ander Oribe
a1aaa0dd41 Refractor (#3836)
* Refractor
* Update AgentService.js
* Update ExportAsJSONActionSpec.js
2021-08-18 11:57:47 -07:00
Andrew Henry
bee15e98c8 Added .npmrc to silence log messages < WARN (#3649)
* Added .npmrc to silence log messages < WARN
2021-08-18 09:31:53 -07:00
Shefali Joshi
092bbe547d Prepare for sprint 1.7.7 (#4127) 2021-08-18 11:56:12 -04:00
Mariusz Rosinski
6cbe05317c 3788 - Re-implement DeviceClassifier in vanilla JS (#3837)
* Changed AgentService to Agent and exposed in utils
* removed MCTDevice
2021-08-17 10:21:50 -07:00
Shefali Joshi
3b92fcdf6c Prevent extra loading of plots data (#4120) 2021-08-17 09:51:53 -07:00
Shefali Joshi
6dde54bd25 Fix plots performance (#4092)
* Fix no mutating props violation for Browsebar and StyleEditor
* Separate plot series data from the configuration (like it should be!)
2021-08-16 14:21:09 -07:00
Nikhil
359e7377ac Notebook Snapshot menu is only updating section and page names for the editor's view #3982 (#4002)
* if default section/page is missing then clear default notebook and hide option from dropdown.

* handle edge case when section is null/undefined.

* refactored notebook localstorage data + fixed some edge cases when default section/page gets deleted.

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-08-12 13:29:01 -07:00
Jamie V
9f4190f781 [Linting] Fix linting errors (#4082) 2021-08-11 15:11:17 -07:00
Jamie V
f3fc991a74 [Telemetry Collections] Add Telemetry Collection Functionality to Telemetry API (#3689)
Adds telemetry collections to the telemetry API

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-08-10 10:36:33 -07:00
Nikhil
2564e75fc9 [Notebook] Example Imagery doesn't capture images #2942 (#2943)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-08-10 10:26:51 -07:00
Shefali Joshi
f42fe78acf 1.7.6 master merge (#4097)
* remove can edit from hyperlink (#4076)

* Add check for stop observing before calling it (#4080)

* Set the yKey value on the series when it's changed (#4083)

* [Imagery] Click on image to get a large view #3582 (#4085)

fixed issue where large imagery view opens only once.

Co-authored-by: Henry Hsu <hhsu0219@gmail.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2021-08-10 07:00:13 -07:00
Nikhil
fe928a1386 [Imagery] Click on image to get a large view #3582 (#4077)
fixed issue where large imagery view opens only once.
2021-08-04 12:57:48 -07:00
121 changed files with 3438 additions and 2630 deletions

View File

@@ -56,14 +56,38 @@ workflows:
browser: ChromeHeadless
always-pass: false
- test:
name: node12-firefoxESR
name: node12-firefoxESR-build-only
node-version: lts/erbium
browser: FirefoxESR
always-pass: true
- test:
name: node14-chrome
name: node14-chrome-build-only
node-version: lts/fermium
browser: ChromeHeadless
always-pass: true
nightly:
jobs:
- test:
name: node10-chrome-nightly
node-version: lts/dubnium
browser: ChromeHeadless
always-pass: false
- test:
name: node12-firefoxESR-nightly
node-version: lts/erbium
browser: FirefoxESR
always-pass: false
- test:
name: node14-chrome-nightly
node-version: lts/fermium
browser: ChromeHeadless
always-pass: false
triggers:
- schedule:
cron: "0 0 * * *"
filters:
branches:
only:
- master

33
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: "CodeQL"
on:
push:
branches: [ master ]
schedule:
- cron: '28 21 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: javascript
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
loglevel=warn

2
API.md
View File

@@ -996,7 +996,7 @@ reveal additional information when the mouse cursor is hovered over it.
A common use case for indicators is to convey the state of some external system such as a
persistence backend or HTTP server. So long as this system is accessible via HTTP request,
Open MCT provides a general purpose indicator to show whether the server is available and
returing a 2xx status code. The URL Status Indicator is made available as a default plugin. See
returning a 2xx status code. The URL Status Indicator is made available as a default plugin. See
the [documentation](./src/plugins/URLIndicatorPlugin) for details on how to install and configure the
URL Status Indicator.

View File

@@ -423,7 +423,7 @@ which can help with this, however.
instead of separate approaches for static and substitutable
dependencies.
* Removes need to understand Angular's DI mechanism.
* Improves useability of documentation (`typeService` is an
* Improves usability of documentation (`typeService` is an
instance of `CompositeService` and implements `TypeService`
so you can easily traverse links in the JSDoc.)
* Can be used more easily from Web Workers, allowing services

View File

@@ -25,7 +25,7 @@
## Legacy Documentation
As we transition to a new API, the following documentation for the old API
(which is supported during the transtion) may be useful as well:
(which is supported during the transition) may be useful as well:
* The [Architecture Overview](architecture/) describes the concepts used
throughout Open MCT, and gives a high level overview of the platform's design.

View File

@@ -63,7 +63,7 @@ define([
StateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = options.end;
var end = Math.min(Date.now(), options.end); // no future values
var duration = domainObject.telemetry.duration * 1000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;

View File

@@ -152,7 +152,7 @@
<h2>How to Use Glyphs</h2>
<div class="cols cols1-1">
<div class="col">
<p>The easiest way to use a glyph is to include its CSS class in an element. The CSS adds a psuedo <code>:before</code> HTML element to whatever element it's attached to that makes proper use of the symbols font.</p>
<p>The easiest way to use a glyph is to include its CSS class in an element. The CSS adds a pseudo <code>:before</code> HTML element to whatever element it's attached to that makes proper use of the symbols font.</p>
<p>Alternately, you can use the <code>.ui-symbol</code> class in an object that contains encoded HTML entities. This method is only recommended if you cannot use the aforementioned CSS class approach.</p>
</div>
<mct-example><a class="s-button icon-gear" title="Settings"></a>

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "1.7.6-SNAPSHOT",
"version": "1.7.8-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {

View File

@@ -64,7 +64,7 @@ define(
*
* @param {DomainObject} domainObject the domain object to navigate to
* @param {Boolean} force if true, force navigation to occur.
* @returns {Boolean} true if navigation occured, otherwise false.
* @returns {Boolean} true if navigation occurred, otherwise false.
*/
NavigationService.prototype.setNavigation = function (domainObject, force) {
if (force) {

View File

@@ -21,28 +21,14 @@
*****************************************************************************/
define([
"./src/MCTDevice",
"./src/AgentService",
"./src/DeviceClassifier"
"./src/AgentService"
], function (
MCTDevice,
AgentService,
DeviceClassifier
AgentService
) {
return {
name: "platform/commonUI/mobile",
definition: {
"extensions": {
"directives": [
{
"key": "mctDevice",
"implementation": MCTDevice,
"depends": [
"agentService"
]
}
],
"services": [
{
"key": "agentService",
@@ -51,15 +37,6 @@ define([
"$window"
]
}
],
"runs": [
{
"implementation": DeviceClassifier,
"depends": [
"agentService",
"$document"
]
}
]
}
}

View File

@@ -20,122 +20,12 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Provides features which support variant behavior on mobile devices.
*
* @namespace platform/commonUI/mobile
*/
define(
[],
function () {
define(["../../../../src/utils/agent/Agent.js"], function (Agent) {
function AngularAgentServiceWrapper(window) {
const AS = Agent.default;
/**
* The query service handles calls for browser and userAgent
* info using a comparison between the userAgent and key
* device names
* @constructor
* @param $window Angular-injected instance of the window
* @memberof platform/commonUI/mobile
*/
function AgentService($window) {
var userAgent = $window.navigator.userAgent,
matches = userAgent.match(/iPad|iPhone|Android/i) || [];
this.userAgent = userAgent;
this.mobileName = matches[0];
this.$window = $window;
this.touchEnabled = ($window.ontouchstart !== undefined);
}
/**
* Check if the user is on a mobile device.
* @returns {boolean} true on mobile
*/
AgentService.prototype.isMobile = function () {
return Boolean(this.mobileName);
};
/**
* Check if the user is on a phone-sized mobile device.
* @returns {boolean} true on a phone
*/
AgentService.prototype.isPhone = function () {
if (this.isMobile()) {
if (this.isAndroidTablet()) {
return false;
} else if (this.mobileName === 'iPad') {
return false;
} else {
return true;
}
} else {
return false;
}
};
/**
* Check if the user is on a tablet sized android device
* @returns {boolean} true on an android tablet
*/
AgentService.prototype.isAndroidTablet = function () {
if (this.mobileName === 'Android') {
if (this.isPortrait() && window.innerWidth >= 768) {
return true;
} else if (this.isLandscape() && window.innerHeight >= 768) {
return true;
}
} else {
return false;
}
};
/**
* Check if the user is on a tablet-sized mobile device.
* @returns {boolean} true on a tablet
*/
AgentService.prototype.isTablet = function () {
return (this.isMobile() && !this.isPhone() && this.mobileName !== 'Android') || (this.isMobile() && this.isAndroidTablet());
};
/**
* Check if the user's device is in a portrait-style
* orientation (display width is narrower than display height.)
* @returns {boolean} true in portrait mode
*/
AgentService.prototype.isPortrait = function () {
return this.$window.innerWidth < this.$window.innerHeight;
};
/**
* Check if the user's device is in a landscape-style
* orientation (display width is greater than display height.)
* @returns {boolean} true in landscape mode
*/
AgentService.prototype.isLandscape = function () {
return !this.isPortrait();
};
/**
* Check if the user's device supports a touch interface.
* @returns {boolean} true if touch is supported
*/
AgentService.prototype.isTouch = function () {
return this.touchEnabled;
};
/**
* Check if the user agent matches a certain named device,
* as indicated by checking for a case-insensitive substring
* match.
* @param {string} name the name to check for
* @returns {boolean} true if the user agent includes that name
*/
AgentService.prototype.isBrowser = function (name) {
name = name.toLowerCase();
return this.userAgent.toLowerCase().indexOf(name) !== -1;
};
return AgentService;
return new AS(window);
}
);
return AngularAgentServiceWrapper;
});

View File

@@ -0,0 +1,96 @@
/*****************************************************************************
* 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 AgentService from "./AgentService";
const TEST_USER_AGENTS = {
DESKTOP:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36",
IPAD:
"Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
IPHONE:
"Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53"
};
describe("The AgentService", function () {
let testWindow;
let agentService;
beforeEach(function () {
testWindow = {
innerWidth: 640,
innerHeight: 480,
navigator: {
userAgent: TEST_USER_AGENTS.DESKTOP
}
};
});
it("recognizes desktop devices as non-mobile", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeFalsy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPhones", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeTruthy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPads", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeTruthy();
});
it("detects display orientation", function () {
agentService = new AgentService(testWindow);
testWindow.innerWidth = 1024;
testWindow.innerHeight = 400;
expect(agentService.isPortrait()).toBeFalsy();
expect(agentService.isLandscape()).toBeTruthy();
testWindow.innerWidth = 400;
testWindow.innerHeight = 1024;
expect(agentService.isPortrait()).toBeTruthy();
expect(agentService.isLandscape()).toBeFalsy();
});
it("detects touch support", function () {
testWindow.ontouchstart = null;
expect(new AgentService(testWindow).isTouch()).toBe(true);
delete testWindow.ontouchstart;
expect(new AgentService(testWindow).isTouch()).toBe(false);
});
it("allows for checking browser type", function () {
testWindow.navigator.userAgent = "Chromezilla Safarifox";
agentService = new AgentService(testWindow);
expect(agentService.isBrowser("Chrome")).toBe(true);
expect(agentService.isBrowser("Firefox")).toBe(false);
});
});

View File

@@ -1,72 +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(
['./DeviceMatchers'],
function (DeviceMatchers) {
/**
* Runs at application startup and adds a subset of the following
* CSS classes to the body of the document, depending on device
* attributes:
*
* * `mobile`: Phones or tablets.
* * `phone`: Phones specifically.
* * `tablet`: Tablets specifically.
* * `desktop`: Non-mobile devices.
* * `portrait`: Devices in a portrait-style orientation.
* * `landscape`: Devices in a landscape-style orientation.
* * `touch`: Device supports touch events.
*
* @param {platform/commonUI/mobile.AgentService} agentService
* the service used to examine the user agent
* @param $document Angular's jqLite-wrapped document element
* @constructor
*/
function MobileClassifier(agentService, $document) {
var body = $document.find('body');
Object.keys(DeviceMatchers).forEach(function (key, index, array) {
if (DeviceMatchers[key](agentService)) {
body.addClass(key);
}
});
if (agentService.isMobile()) {
var mediaQuery = window.matchMedia('(orientation: landscape)');
mediaQuery.addListener(function (event) {
if (event.matches) {
body.removeClass('portrait');
body.addClass('landscape');
} else {
body.removeClass('landscape');
body.addClass('portrait');
}
});
}
}
return MobileClassifier;
}
);

View File

@@ -1,88 +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(
['./DeviceMatchers'],
function (DeviceMatchers) {
/**
* The `mct-device` directive, when applied as an attribute,
* only includes the element when the device being used matches
* a set of characteristics required.
*
* Required characteristics are given as space-separated strings
* as the value to this attribute, e.g.:
*
* <span mct-device="mobile portrait">Hello world!</span>
*
* ...will only show Hello world! when viewed on a mobile device
* in the portrait orientation.
*
* Valid device characteristics to detect are:
*
* * `mobile`: Phones or tablets.
* * `phone`: Phones specifically.
* * `tablet`: Tablets specifically.
* * `desktop`: Non-mobile devices.
* * `portrait`: Devices in a portrait-style orientation.
* * `landscape`: Devices in a landscape-style orientation.
* * `touch`: Device supports touch events.
*
* @param {AgentService} agentService used to detect device type
* based on information about the user agent
*/
function MCTDevice(agentService) {
function deviceMatches(tokens) {
tokens = tokens || "";
return tokens.split(" ").every(function (token) {
var fn = DeviceMatchers[token];
return fn && fn(agentService);
});
}
function link(scope, element, attrs, ctrl, transclude) {
if (deviceMatches(attrs.mctDevice)) {
transclude(function (clone) {
element.replaceWith(clone);
});
}
}
return {
link: link,
// We are transcluding the whole element (like ng-if)
transclude: 'element',
// 1 more than ng-if
priority: 601,
// Also terminal, since element will be transcluded
terminal: true,
// Only apply as an attribute
restrict: "A"
};
}
return MCTDevice;
}
);

View File

@@ -1,99 +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/AgentService"],
function (AgentService) {
var TEST_USER_AGENTS = {
DESKTOP: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36",
IPAD: "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
IPHONE: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53"
};
describe("The AgentService", function () {
var testWindow, agentService;
beforeEach(function () {
testWindow = {
innerWidth: 640,
innerHeight: 480,
navigator: {
userAgent: TEST_USER_AGENTS.DESKTOP
}
};
});
it("recognizes desktop devices as non-mobile", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeFalsy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPhones", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeTruthy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPads", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeTruthy();
});
it("detects display orientation", function () {
agentService = new AgentService(testWindow);
testWindow.innerWidth = 1024;
testWindow.innerHeight = 400;
expect(agentService.isPortrait()).toBeFalsy();
expect(agentService.isLandscape()).toBeTruthy();
testWindow.innerWidth = 400;
testWindow.innerHeight = 1024;
expect(agentService.isPortrait()).toBeTruthy();
expect(agentService.isLandscape()).toBeFalsy();
});
it("detects touch support", function () {
testWindow.ontouchstart = null;
expect(new AgentService(testWindow).isTouch())
.toBe(true);
delete testWindow.ontouchstart;
expect(new AgentService(testWindow).isTouch())
.toBe(false);
});
it("allows for checking browser type", function () {
testWindow.navigator.userAgent = "Chromezilla Safarifox";
agentService = new AgentService(testWindow);
expect(agentService.isBrowser("Chrome")).toBe(true);
expect(agentService.isBrowser("Firefox")).toBe(false);
});
});
}
);

View File

@@ -1,109 +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/DeviceClassifier", "../src/DeviceMatchers"],
function (DeviceClassifier, DeviceMatchers) {
var AGENT_SERVICE_METHODS = [
'isMobile',
'isPhone',
'isTablet',
'isPortrait',
'isLandscape',
'isTouch'
],
TEST_PERMUTATIONS = [
['isMobile', 'isPhone', 'isTouch', 'isPortrait'],
['isMobile', 'isPhone', 'isTouch', 'isLandscape'],
['isMobile', 'isTablet', 'isTouch', 'isPortrait'],
['isMobile', 'isTablet', 'isTouch', 'isLandscape'],
['isTouch'],
[]
];
describe("DeviceClassifier", function () {
var mockAgentService,
mockDocument,
mockBody;
beforeEach(function () {
mockAgentService = jasmine.createSpyObj(
'agentService',
AGENT_SERVICE_METHODS
);
mockDocument = jasmine.createSpyObj(
'$document',
['find']
);
mockBody = jasmine.createSpyObj(
'body',
['addClass']
);
mockDocument.find.and.callFake(function (sel) {
return sel === 'body' && mockBody;
});
AGENT_SERVICE_METHODS.forEach(function (m) {
mockAgentService[m].and.returnValue(false);
});
});
TEST_PERMUTATIONS.forEach(function (trueMethods) {
var summary = trueMethods.length === 0
? "device has no detected characteristics"
: "device " + (trueMethods.join(", "));
describe("when " + summary, function () {
var classifier; // eslint-disable-line
beforeEach(function () {
trueMethods.forEach(function (m) {
mockAgentService[m].and.returnValue(true);
});
classifier = new DeviceClassifier(
mockAgentService,
mockDocument
);
});
it("adds classes for matching, detected characteristics", function () {
Object.keys(DeviceMatchers).filter(function (m) {
return DeviceMatchers[m](mockAgentService);
}).forEach(function (key) {
expect(mockBody.addClass)
.toHaveBeenCalledWith(key);
});
});
it("does not add classes for non-matching characteristics", function () {
Object.keys(DeviceMatchers).filter(function (m) {
return !DeviceMatchers[m](mockAgentService);
}).forEach(function (key) {
expect(mockBody.addClass)
.not.toHaveBeenCalledWith(key);
});
});
});
});
});
}
);

View File

@@ -1,78 +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/DeviceMatchers"],
function (DeviceMatchers) {
describe("DeviceMatchers", function () {
var mockAgentService;
beforeEach(function () {
mockAgentService = jasmine.createSpyObj(
'agentService',
[
'isMobile',
'isPhone',
'isTablet',
'isPortrait',
'isLandscape',
'isTouch'
]
);
});
it("detects when a device is a desktop device", function () {
mockAgentService.isMobile.and.returnValue(false);
expect(DeviceMatchers.desktop(mockAgentService))
.toBe(true);
mockAgentService.isMobile.and.returnValue(true);
expect(DeviceMatchers.desktop(mockAgentService))
.toBe(false);
});
function method(deviceType) {
return "is" + deviceType[0].toUpperCase() + deviceType.slice(1);
}
[
"mobile",
"phone",
"tablet",
"landscape",
"portrait",
"landscape",
"touch"
].forEach(function (deviceType) {
it("detects when a device is a " + deviceType + " device", function () {
mockAgentService[method(deviceType)].and.returnValue(true);
expect(DeviceMatchers[deviceType](mockAgentService))
.toBe(true);
mockAgentService[method(deviceType)].and.returnValue(false);
expect(DeviceMatchers[deviceType](mockAgentService))
.toBe(false);
});
});
});
}
);

View File

@@ -1,168 +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/MCTDevice'],
function (MCTDevice) {
var JQLITE_METHODS = ['replaceWith'];
describe("The mct-device directive", function () {
var mockAgentService,
mockTransclude,
mockElement,
mockClone,
testAttrs,
directive;
function link() {
directive.link(null, mockElement, testAttrs, null, mockTransclude);
}
beforeEach(function () {
mockAgentService = jasmine.createSpyObj(
"agentService",
["isMobile", "isPhone", "isTablet", "isPortrait", "isLandscape"]
);
mockTransclude = jasmine.createSpy("$transclude");
mockElement = jasmine.createSpyObj(name, JQLITE_METHODS);
mockClone = jasmine.createSpyObj(name, JQLITE_METHODS);
mockTransclude.and.callFake(function (fn) {
fn(mockClone);
});
// Look desktop-like by default
mockAgentService.isLandscape.and.returnValue(true);
testAttrs = {};
directive = new MCTDevice(mockAgentService);
});
function expectInclusion() {
expect(mockElement.replaceWith)
.toHaveBeenCalledWith(mockClone);
}
function expectExclusion() {
expect(mockElement.replaceWith).not.toHaveBeenCalled();
}
it("is applicable at the attribute level", function () {
expect(directive.restrict).toEqual("A");
});
it("transcludes at the element level", function () {
expect(directive.transclude).toEqual('element');
});
it("has a greater priority number than ng-if", function () {
expect(directive.priority > 600).toBeTruthy();
});
it("restricts element inclusion for mobile devices", function () {
testAttrs.mctDevice = "mobile";
link();
expectExclusion();
mockAgentService.isMobile.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for tablet devices", function () {
testAttrs.mctDevice = "tablet";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isTablet.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for phone devices", function () {
testAttrs.mctDevice = "phone";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isPhone.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for desktop devices", function () {
testAttrs.mctDevice = "desktop";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isMobile.and.returnValue(false);
link();
expectInclusion();
});
it("restricts element inclusion for portrait orientation", function () {
testAttrs.mctDevice = "portrait";
link();
expectExclusion();
mockAgentService.isPortrait.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for landscape orientation", function () {
testAttrs.mctDevice = "landscape";
mockAgentService.isLandscape.and.returnValue(false);
mockAgentService.isPortrait.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isLandscape.and.returnValue(true);
link();
expectInclusion();
});
it("allows multiple device characteristics to be requested", function () {
// Won't try to test every permutation here, just
// make sure the multi-characteristic feature has support.
testAttrs.mctDevice = "portrait mobile";
link();
// Neither portrait nor mobile, not called
expectExclusion();
mockAgentService.isPortrait.and.returnValue(true);
link();
// Was portrait, but not mobile, so no
expectExclusion();
mockAgentService.isMobile.and.returnValue(true);
link();
expectInclusion();
});
});
}
);

View File

@@ -379,7 +379,7 @@ define([
{
"name": "Math.uuid.js",
"version": "1.4.7",
"description": "Unique identifer generation (code adapted.)",
"description": "Unique identifier generation (code adapted.)",
"author": "Robert Kieffer",
"website": "https://github.com/broofa/node-uuid",
"copyright": "Copyright (c) 2010-2012 Robert Kieffer",

View File

@@ -30,8 +30,8 @@ define([
return function ImportExportPlugin() {
return function (openmct) {
ExportAsJSONAction.appliesTo = function (context) {
return openmct.$injector.get('policyService')
ExportAsJSONAction.prototype.appliesTo = function (context) {
return this.openmct.$injector.get('policyService')
.allow("creation", context.domainObject.getCapability("type")
);
};

View File

@@ -29,7 +29,7 @@ define(
],
function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) {
xdescribe("The export JSON action", function () {
describe("The export JSON action", function () {
var context,
action,
@@ -102,7 +102,7 @@ define(
expect(action).toBeDefined();
});
it("doesn't export non-creatable objects in tree", function () {
xit("doesn't export non-creatable objects in tree", function () {
var nonCreatableType = {
hasFeature:
function (feature) {
@@ -149,7 +149,7 @@ define(
});
});
it("can export self-containing objects", function () {
xit("can export self-containing objects", function () {
var parent = domainObjectFactory({
name: 'parent',
model: {
@@ -191,7 +191,7 @@ define(
});
});
it("exports links to external objects as new objects", function () {
xit("exports links to external objects as new objects", function () {
var parent = domainObjectFactory({
name: 'parent',
model: {

View File

@@ -27,7 +27,7 @@ define(
],
function (ImportAsJSONAction, domainObjectFactory) {
xdescribe("The import JSON action", function () {
describe("The import JSON action", function () {
var context = {};
var action,
@@ -146,7 +146,7 @@ define(
});
});
it("can import self-containing objects", function () {
xit("can import self-containing objects", function () {
var compDomainObject = domainObjectFactory({
name: 'compObject',
model: { name: 'compObject'},
@@ -198,7 +198,7 @@ define(
});
});
it("assigns new ids to each imported object", function () {
xit("assigns new ids to each imported object", function () {
dialogService.getUserInput.and.returnValue(Promise.resolve(
{
selectFile: {

View File

@@ -47,7 +47,7 @@ define(
* @param $interval Angular's $interval service
* @param {string} space the name of the persistence space being served
* @param {string} root the root of the path to ElasticSearch
* @param {stirng} path the path to domain objects within ElasticSearch
* @param {string} path the path to domain objects within ElasticSearch
*/
function ElasticPersistenceProvider($http, $q, space, root, path) {
this.spaces = [space];

View File

@@ -287,6 +287,7 @@ define([
this.install(this.plugins.ViewLargeAction());
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.NonEditableFolder());
this.install(this.plugins.DeviceClassifier());
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@@ -60,9 +60,7 @@ class ActionsAPI extends EventEmitter {
}
_getCachedActionCollection(objectPath, view) {
let cachedActionCollection = this._actionCollections.get(view);
return cachedActionCollection;
return this._actionCollections.get(view);
}
_newActionCollection(objectPath, view, skipEnvironmentObservers) {

View File

@@ -42,7 +42,7 @@ import EventEmitter from 'EventEmitter';
*
* @typedef {object} NotificationModel
* @property {string} message The message to be displayed by the notification
* @property {number | 'unknown'} [progress] The progres of some ongoing task. Should be a number between 0 and 100, or
* @property {number | 'unknown'} [progress] The progress of some ongoing task. Should be a number between 0 and 100, or
* with the string literal 'unknown'.
* @property {string} [progressText] A message conveying progress of some ongoing task.
@@ -98,7 +98,7 @@ export default class NotificationAPI extends EventEmitter {
* Present an alert to the user.
* @param {string} message The message to display to the user.
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link
@@ -119,7 +119,7 @@ export default class NotificationAPI extends EventEmitter {
* Present an error message to the user
* @param {string} message
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link

View File

@@ -358,6 +358,20 @@ ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
return domainObject;
};
/**
* Return relative url path from a given object path
* eg: #/browse/mine/cb56f6bf-c900-43b7-b923-2e3b64b412db/6e89e858-77ce-46e4-a1ad-749240286497/....
* @param {Array} objectPath
* @returns {string} relative url for object
*/
ObjectAPI.prototype.getRelativePath = function (objectPath) {
return objectPath
.map(p => this.makeKeyString(p.identifier))
.reverse()
.join('/')
;
};
/**
* Modify a domain object.
* @param {module:openmct.DomainObject} object the object to mutate

View File

@@ -10,28 +10,37 @@ const cssClasses = {
};
class Overlay extends EventEmitter {
constructor(options) {
constructor({
buttons,
autoHide = true,
dismissable = true,
element,
onDestroy,
size
} = {}) {
super();
this.dismissable = options.dismissable !== false;
this.container = document.createElement('div');
this.container.classList.add('l-overlay-wrapper', cssClasses[options.size]);
this.container.classList.add('l-overlay-wrapper', cssClasses[size]);
this.autoHide = autoHide;
this.dismissable = dismissable !== false;
this.component = new Vue({
provide: {
dismiss: this.dismiss.bind(this),
element: options.element,
buttons: options.buttons,
dismissable: this.dismissable
},
components: {
OverlayComponent: OverlayComponent
},
provide: {
dismiss: this.dismiss.bind(this),
element,
buttons,
dismissable: this.dismissable
},
template: '<overlay-component></overlay-component>'
});
if (options.onDestroy) {
this.once('destroy', options.onDestroy);
if (onDestroy) {
this.once('destroy', onDestroy);
}
}

View File

@@ -30,7 +30,10 @@ class OverlayAPI {
*/
showOverlay(overlay) {
if (this.activeOverlays.length) {
this.activeOverlays[this.activeOverlays.length - 1].container.classList.add('invisible');
const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (previousOverlay.autoHide) {
previousOverlay.container.classList.add('invisible');
}
}
this.activeOverlays.push(overlay);
@@ -60,7 +63,7 @@ class OverlayAPI {
* A description of option properties that can be passed into the overlay
* @typedef options
* @property {object} element DOMElement that is to be inserted/shown on the overlay
* @property {string} size prefered size of the overlay (large, small, fit)
* @property {string} size preferred size of the overlay (large, small, fit)
* @property {array} buttons optional button objects with label and callback properties
* @property {function} onDestroy callback to be called when overlay is destroyed
* @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away

View File

@@ -12,7 +12,7 @@
></button>
<div
ref="element"
class="c-overlay__contents"
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0"
></div>
<div

View File

@@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { TelemetryCollection } = require("./TelemetryCollection");
define([
'../../plugins/displayLayout/CustomStringFormatter',
'./TelemetryMetadataManager',
@@ -273,6 +275,28 @@ define([
}
};
/**
* Request telemetry collection for a domain object.
* The `options` argument allows you to specify filters
* (start, end, etc.), sort order, and strategies for retrieving
* telemetry (aggregation, latest available, etc.).
*
* @method requestCollection
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
* @param {module:openmct.DomainObject} domainObject the object
* which has associated telemetry
* @param {module:openmct.TelemetryAPI~TelemetryRequest} options
* options for this telemetry collection request
* @returns {TelemetryCollection} a TelemetryCollection instance
*/
TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) {
return new TelemetryCollection(
this.openmct,
domainObject,
options
);
};
/**
* Request historical telemetry for a domain object.
* The `options` argument allows you to specify filters

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,388 @@
/*****************************************************************************
* 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 _ from 'lodash';
import EventEmitter from 'EventEmitter';
const ERRORS = {
TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.',
LOADED: 'Telemetry Collection has already been loaded.'
};
/** Class representing a Telemetry Collection. */
export class TelemetryCollection extends EventEmitter {
/**
* Creates a Telemetry Collection
*
* @param {object} openmct - Openm MCT
* @param {object} domainObject - Domain Object to user for telemetry collection
* @param {object} options - Any options passed in for request/subscribe
*/
constructor(openmct, domainObject, options) {
super();
this.loaded = false;
this.openmct = openmct;
this.domainObject = domainObject;
this.boundedTelemetry = [];
this.futureBuffer = [];
this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined;
this.historicalProvider = undefined;
this.options = options;
this.pageState = undefined;
this.lastBounds = undefined;
this.requestAbort = undefined;
}
/**
* This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/
load() {
if (this.loaded) {
this._error(ERRORS.LOADED);
}
this._timeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds();
this._watchBounds();
this._watchTimeSystem();
this._initiateHistoricalRequests();
this._initiateSubscriptionTelemetry();
this.loaded = true;
}
/**
* can/should be called by the requester of the telemetry collection
* to remove any listeners
*/
destroy() {
if (this.requestAbort) {
this.requestAbort.abort();
}
this._unwatchBounds();
this._unwatchTimeSystem();
if (this.unsubscribe) {
this.unsubscribe();
}
this.removeAllListeners();
}
/**
* This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/
getAll() {
return this.boundedTelemetry;
}
/**
* Sets up the telemetry collection for historical requests,
* this uses the "standardizeRequestOptions" from Telemetry API
* @private
*/
_initiateHistoricalRequests() {
this.openmct.telemetry.standardizeRequestOptions(this.options);
this.historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, this.options);
this._requestHistoricalTelemetry();
}
/**
* If a historical provider exists, then historical requests will be made
* @private
*/
async _requestHistoricalTelemetry() {
if (!this.historicalProvider) {
return;
}
let historicalData;
try {
this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal;
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
this.requestAbort = undefined;
} catch (error) {
console.error('Error requesting telemetry data...');
this.requestAbort = undefined;
this._error(error);
}
this._processNewTelemetry(historicalData);
}
/**
* This uses the built in subscription function from Telemetry API
* @private
*/
_initiateSubscriptionTelemetry() {
if (this.unsubscribe) {
this.unsubscribe();
}
this.unsubscribe = this.openmct.telemetry
.subscribe(
this.domainObject,
datum => this._processNewTelemetry(datum),
this.options
);
}
/**
* Filter any new telemetry (add/page, historical, subscription) based on
* time bounds and dupes
*
* @param {(Object|Object[])} telemetryData - telemetry data object or
* array of telemetry data objects
* @private
*/
_processNewTelemetry(telemetryData) {
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
let afterEndOfBounds;
let added = [];
for (let datum of data) {
parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start;
afterEndOfBounds = parsedValue > this.lastBounds.end;
if (!afterEndOfBounds && !beforeStartOfBounds) {
let isDuplicate = false;
let startIndex = this._sortedIndex(datum);
let endIndex = undefined;
// dupe check
if (startIndex !== this.boundedTelemetry.length) {
endIndex = _.sortedLastIndexBy(
this.boundedTelemetry,
datum,
boundedDatum => this.parseTime(boundedDatum)
);
if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
}
}
if (!isDuplicate) {
let index = endIndex || startIndex;
this.boundedTelemetry.splice(index, 0, datum);
added.push(datum);
}
} else if (afterEndOfBounds) {
this.futureBuffer.push(datum);
}
}
if (added.length) {
this.emit('add', added);
}
}
/**
* Finds the correct insertion point for the given telemetry datum.
* Leverages lodash's `sortedIndexBy` function which implements a binary search.
* @private
*/
_sortedIndex(datum) {
if (this.boundedTelemetry.length === 0) {
return 0;
}
let parsedValue = this.parseTime(datum);
let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]);
if (parsedValue > lastValue || parsedValue === lastValue) {
return this.boundedTelemetry.length;
} else {
return _.sortedIndexBy(
this.boundedTelemetry,
datum,
boundedDatum => this.parseTime(boundedDatum)
);
}
}
/**
* when the start time, end time, or both have been updated.
* data could be added OR removed here we update the current
* bounded telemetry
*
* @param {TimeConductorBounds} bounds The newly updated bounds
* @param {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
* @private
*/
_bounds(bounds, isTick) {
let startChanged = this.lastBounds.start !== bounds.start;
let endChanged = this.lastBounds.end !== bounds.end;
this.lastBounds = bounds;
if (isTick) {
// need to check futureBuffer and need to check
// if anything has fallen out of bounds
let startIndex = 0;
let endIndex = 0;
let discarded = [];
let added = [];
let testDatum = {};
if (startChanged) {
testDatum[this.timeKey] = bounds.start;
// Calculate the new index of the first item within the bounds
startIndex = _.sortedIndexBy(
this.boundedTelemetry,
testDatum,
datum => this.parseTime(datum)
);
discarded = this.boundedTelemetry.splice(0, startIndex);
}
if (endChanged) {
testDatum[this.timeKey] = bounds.end;
// Calculate the new index of the last item in bounds
endIndex = _.sortedLastIndexBy(
this.futureBuffer,
testDatum,
datum => this.parseTime(datum)
);
added = this.futureBuffer.splice(0, endIndex);
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
}
if (discarded.length > 0) {
this.emit('remove', discarded);
}
if (added.length > 0) {
this.emit('add', added);
}
} else {
// user bounds change, reset
this._reset();
}
}
/**
* whenever the time system is updated need to update related values in
* the Telemetry Collection and reset the telemetry collection
*
* @param {TimeSystem} timeSystem - the value of the currently applied
* Time System
* @private
*/
_timeSystem(timeSystem) {
let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key);
if (domain === undefined) {
this._error(ERRORS.TIMESYSTEM_KEY);
}
// timeKey is used to create a dummy datum used for sorting
this.timeKey = domain.source; // this defaults to key if no source is set
let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
this.parseTime = (datum) => {
return valueFormatter.parse(datum);
};
this._reset();
}
/**
* Reset the telemetry data of the collection, and re-request
* historical telemetry
* @private
*
* @todo handle subscriptions more granually
*/
_reset() {
this.boundedTelemetry = [];
this.futureBuffer = [];
this._requestHistoricalTelemetry();
}
/**
* adds the _bounds callback to the 'bounds' timeAPI listener
* @private
*/
_watchBounds() {
this.openmct.time.on('bounds', this._bounds, this);
}
/**
* removes the _bounds callback from the 'bounds' timeAPI listener
* @private
*/
_unwatchBounds() {
this.openmct.time.off('bounds', this._bounds, this);
}
/**
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener
* @private
*/
_watchTimeSystem() {
this.openmct.time.on('timeSystem', this._timeSystem, this);
}
/**
* removes the _timeSystem callback from the 'timeSystem' timeAPI listener
* @private
*/
_unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this._timeSystem, this);
}
/**
* will throw a new Error, for passed in message
* @param {string} message Message describing the error
* @private
*/
_error(message) {
throw new Error(message);
}
}

View File

@@ -78,6 +78,9 @@ class ImageExporter {
}
return html2canvas(element, {
useCORS: true,
allowTaint: true,
logging: false,
onclone: function (document) {
if (className) {
const clonedElement = document.getElementById(exportId);
@@ -87,7 +90,7 @@ class ImageExporter {
element.id = oldId;
},
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) {
}).then(canvas => {
dialog.dismiss();
return new Promise(function (resolve, reject) {
@@ -102,9 +105,10 @@ class ImageExporter {
return canvas.toBlob(blob => resolve({ blob }), mimeType);
});
}, function (error) {
console.log('error capturing image', error);
}).catch(error => {
dialog.dismiss();
console.error('error capturing image', error);
const errorDialog = overlays.dialog({
iconClass: 'error',
message: 'Image was not captured successfully!',

View File

@@ -19,23 +19,14 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Agent from "../../utils/agent/Agent";
import DeviceClassifier from "./src/DeviceClassifier";
export default class ClearDataAction {
constructor(openmct, appliesToObjects) {
this.name = 'Clear Data for Object';
this.key = 'clear-data-action';
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
this.cssClass = 'icon-clear-data';
this._openmct = openmct;
this._appliesToObjects = appliesToObjects;
}
invoke(objectPath) {
this._openmct.objectViews.emit('clearData', objectPath[0]);
}
appliesTo(objectPath) {
let contextualDomainObject = objectPath[0];
return this._appliesToObjects.filter(type => contextualDomainObject.type === type).length;
}
}
export default () => {
return (openmct) => {
openmct.on("start", () => {
const agent = new Agent(window);
DeviceClassifier(agent, window.document);
});
};
};

View File

@@ -0,0 +1,72 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/**
* Runs at application startup and adds a subset of the following
* CSS classes to the body of the document, depending on device
* attributes:
*
* * `mobile`: Phones or tablets.
* * `phone`: Phones specifically.
* * `tablet`: Tablets specifically.
* * `desktop`: Non-mobile devices.
* * `portrait`: Devices in a portrait-style orientation.
* * `landscape`: Devices in a landscape-style orientation.
* * `touch`: Device supports touch events.
*
* @param {utils/agent/Agent} agent
* the service used to examine the user agent
* @param document the HTML DOM document object
* @constructor
*/
import DeviceMatchers from "./DeviceMatchers";
export default (agent, document) => {
const body = document.body;
Object.keys(DeviceMatchers).forEach((key, index, array) => {
if (DeviceMatchers[key](agent)) {
body.classList.add(key);
}
});
if (agent.isMobile()) {
const mediaQuery = window.matchMedia("(orientation: landscape)");
function eventHandler(event) {
console.log("changed");
if (event.matches) {
body.classList.remove("portrait");
body.classList.add("landscape");
} else {
body.classList.remove("landscape");
body.classList.add("portrait");
}
}
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener(`change`, eventHandler);
} else {
// Deprecated 'MediaQueryList' API, <Safari 14, IE, <Edge 16
mediaQuery.addListener(eventHandler);
}
}
};

View File

@@ -0,0 +1,105 @@
/*****************************************************************************
* 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 DeviceClassifier from "./DeviceClassifier";
import DeviceMatchers from "./DeviceMatchers";
const AGENT_METHODS = [
"isMobile",
"isPhone",
"isTablet",
"isPortrait",
"isLandscape",
"isTouch"
];
const TEST_PERMUTATIONS = [
["isMobile", "isPhone", "isTouch", "isPortrait"],
["isMobile", "isPhone", "isTouch", "isLandscape"],
["isMobile", "isTablet", "isTouch", "isPortrait"],
["isMobile", "isTablet", "isTouch", "isLandscape"],
["isTouch"],
[]
];
describe("DeviceClassifier", function () {
let mockAgent;
let mockDocument;
let mockClassList;
beforeEach(function () {
mockAgent = jasmine.createSpyObj(
"agent",
AGENT_METHODS
);
mockClassList = jasmine.createSpyObj("classList", ["add"]);
mockDocument = jasmine.createSpyObj(
"document",
{},
{ body: { classList: mockClassList } }
);
AGENT_METHODS.forEach(function (m) {
mockAgent[m].and.returnValue(false);
});
});
TEST_PERMUTATIONS.forEach(function (trueMethods) {
const summary =
trueMethods.length === 0
? "device has no detected characteristics"
: "device " + trueMethods.join(", ");
describe("when " + summary, function () {
beforeEach(function () {
trueMethods.forEach(function (m) {
mockAgent[m].and.returnValue(true);
});
// eslint-disable-next-line no-new
DeviceClassifier(mockAgent, mockDocument);
});
it("adds classes for matching, detected characteristics", function () {
Object.keys(DeviceMatchers)
.filter(function (m) {
return DeviceMatchers[m](mockAgent);
})
.forEach(function (key) {
expect(mockDocument.body.classList.add).toHaveBeenCalledWith(key);
});
});
it("does not add classes for non-matching characteristics", function () {
Object.keys(DeviceMatchers)
.filter(function (m) {
return !DeviceMatchers[m](mockAgent);
})
.forEach(function (key) {
expect(mockDocument.body.classList.add).not.toHaveBeenCalledWith(
key
);
});
});
});
});
});

View File

@@ -19,40 +19,39 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(function () {
/**
* An object containing key-value pairs, where keys are symbolic of
* device attributes, and values are functions that take the
* `agentService` as inputs and return boolean values indicating
* whether or not the current device has these attributes.
*
* For internal use by the mobile support bundle.
*
* @memberof platform/commonUI/mobile
* @private
*/
return {
mobile: function (agentService) {
return agentService.isMobile();
},
phone: function (agentService) {
return agentService.isPhone();
},
tablet: function (agentService) {
return agentService.isTablet();
},
desktop: function (agentService) {
return !agentService.isMobile();
},
portrait: function (agentService) {
return agentService.isPortrait();
},
landscape: function (agentService) {
return agentService.isLandscape();
},
touch: function (agentService) {
return agentService.isTouch();
}
};
});
/**
* An object containing key-value pairs, where keys are symbolic of
* device attributes, and values are functions that take the
* `agent` as inputs and return boolean values indicating
* whether or not the current device has these attributes.
*
* For internal use by the mobile support bundle.
*
* @memberof src/plugins/DeviceClassifier
* @private
*/
export default {
mobile: function (agent) {
return agent.isMobile();
},
phone: function (agent) {
return agent.isPhone();
},
tablet: function (agent) {
return agent.isTablet();
},
desktop: function (agent) {
return !agent.isMobile();
},
portrait: function (agent) {
return agent.isPortrait();
},
landscape: function (agent) {
return agent.isLandscape();
},
touch: function (agent) {
return agent.isTouch();
}
};

View File

@@ -0,0 +1,65 @@
/*****************************************************************************
* 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 DeviceMatchers from "./DeviceMatchers";
describe("DeviceMatchers", function () {
let mockAgent;
beforeEach(function () {
mockAgent = jasmine.createSpyObj("agent", [
"isMobile",
"isPhone",
"isTablet",
"isPortrait",
"isLandscape",
"isTouch"
]);
});
it("detects when a device is a desktop device", function () {
mockAgent.isMobile.and.returnValue(false);
expect(DeviceMatchers.desktop(mockAgent)).toBe(true);
mockAgent.isMobile.and.returnValue(true);
expect(DeviceMatchers.desktop(mockAgent)).toBe(false);
});
function method(deviceType) {
return "is" + deviceType[0].toUpperCase() + deviceType.slice(1);
}
[
"mobile",
"phone",
"tablet",
"landscape",
"portrait",
"landscape",
"touch"
].forEach(function (deviceType) {
it("detects when a device is a " + deviceType + " device", function () {
mockAgent[method(deviceType)].and.returnValue(true);
expect(DeviceMatchers[deviceType](mockAgent)).toBe(true);
mockAgent[method(deviceType)].and.returnValue(false);
expect(DeviceMatchers[deviceType](mockAgent)).toBe(false);
});
});
});

View File

@@ -0,0 +1,78 @@
/*****************************************************************************
* 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.
*****************************************************************************/
function inSelectionPath(openmct, domainObject) {
const domainObjectIdentifier = domainObject.identifier;
return openmct.selection.get().some(selectionPath => {
return selectionPath.some(objectInPath => {
const objectInPathIdentifier = objectInPath.context.item.identifier;
return openmct.objects.areIdsEqual(objectInPathIdentifier, domainObjectIdentifier);
});
});
}
export default class ClearDataAction {
constructor(openmct, appliesToObjects) {
this.name = 'Clear Data for Object';
this.key = 'clear-data-action';
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
this.cssClass = 'icon-clear-data';
this._openmct = openmct;
this._appliesToObjects = appliesToObjects;
}
invoke(objectPath) {
let domainObject = null;
if (objectPath) {
domainObject = objectPath[0];
}
this._openmct.objectViews.emit('clearData', domainObject);
}
appliesTo(objectPath) {
if (!objectPath) {
return false;
}
const contextualDomainObject = objectPath[0];
// first check to see if this action applies to this sort of object at all
const appliesToThisObject = this._appliesToObjects.some(type => {
return contextualDomainObject.type === type;
});
if (!appliesToThisObject) {
// we've selected something not applicable
return false;
}
const objectInSelectionPath = inSelectionPath(this._openmct, contextualDomainObject);
if (objectInSelectionPath) {
return true;
} else {
// if this it doesn't match up, check to see if we're in a composition (i.e., layout)
const routerPath = this._openmct.router.path[0];
return routerPath.type === 'layout';
}
}
}

View File

@@ -22,7 +22,7 @@
define([
'./components/globalClearIndicator.vue',
'./clearDataAction',
'./ClearDataAction',
'vue'
], function (
GlobaClearIndicator,

View File

@@ -0,0 +1,140 @@
/*****************************************************************************
* 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 ClearDataActionPlugin from '../plugin.js';
import ClearDataAction from '../ClearDataAction.js';
describe('When the Clear Data Plugin is installed,', () => {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const goodMockSelectionPath = [[{
context: {
item: {
identifier: {
key: 'apple',
namespace: ''
}
}
}
}]];
const openmct = {
objectViews: mockObjectViews,
indicators: mockIndicatorProvider,
actions: mockActionsProvider,
install: function (plugin) {
plugin(this);
},
selection: {
get: function () {
return goodMockSelectionPath;
}
},
objects: {
areIdsEqual: function (obj1, obj2) {
return true;
}
}
};
const mockObjectPath = [
{
name: 'mockObject1',
type: 'apple'
},
{
name: 'mockObject2',
type: 'banana'
}
];
it('Global Clear Indicator is installed', () => {
openmct.install(ClearDataActionPlugin([]));
expect(mockIndicatorProvider.add).toHaveBeenCalled();
});
it('Clear Data context menu action is installed', () => {
openmct.install(ClearDataActionPlugin([]));
expect(mockActionsProvider.register).toHaveBeenCalled();
});
it('clear data action emits a clearData event when invoked', () => {
const action = new ClearDataAction(openmct);
action.invoke(mockObjectPath);
expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]);
});
it('clears data on applicable objects', () => {
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(true);
});
it('does not clear data on inapplicable objects', () => {
let action = new ClearDataAction(openmct, ['pineapple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(false);
});
it('does not clear data if not in the selection path and not a layout', () => {
openmct.objects = {
areIdsEqual: function (obj1, obj2) {
return false;
}
};
openmct.router = {
path: [{type: 'not-a-layout'}]
};
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(false);
});
it('does clear data if not in the selection path and is a layout', () => {
openmct.objects = {
areIdsEqual: function (obj1, obj2) {
return false;
}
};
openmct.router = {
path: [{type: 'layout'}]
};
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(true);
});
});

View File

@@ -1,64 +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.
*****************************************************************************/
import ClearDataActionPlugin from '../plugin.js';
import ClearDataAction from '../clearDataAction.js';
describe('When the Clear Data Plugin is installed,', function () {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const openmct = {
objectViews: mockObjectViews,
indicators: mockIndicatorProvider,
actions: mockActionsProvider,
install: function (plugin) {
plugin(this);
}
};
const mockObjectPath = [
{name: 'mockObject1'},
{name: 'mockObject2'}
];
it('Global Clear Indicator is installed', function () {
openmct.install(ClearDataActionPlugin([]));
expect(mockIndicatorProvider.add).toHaveBeenCalled();
});
it('Clear Data context menu action is installed', function () {
openmct.install(ClearDataActionPlugin([]));
expect(mockActionsProvider.register).toHaveBeenCalled();
});
it('clear data action emits a clearData event when invoked', function () {
let action = new ClearDataAction(openmct);
action.invoke(mockObjectPath);
expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]);
});
});

View File

@@ -273,10 +273,7 @@ export default {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => {
this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.objectPath
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath);
}
);
},

View File

@@ -141,6 +141,7 @@ const NON_STYLEABLE_CONTAINER_TYPES = [
const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [
'line-view',
'box-view',
'ellipse-view',
'image-view'
];
@@ -296,10 +297,7 @@ export default {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => {
this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.objectPath
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath);
}
);
},
@@ -321,7 +319,7 @@ export default {
if (item) {
const type = this.openmct.types.get(item.type);
if (type && type.definition) {
creatable = (type.definition.creatable === true);
creatable = (type.definition.creatable !== undefined && (type.definition.creatable === 'true' || type.definition.creatable === true));
}
}

View File

@@ -230,7 +230,7 @@ describe('the plugin', function () {
};
const staticStyle = {
"style": {
"backgroundColor": "#717171",
"backgroundColor": "#666666",
"border": "1px solid #00ffff"
}
};
@@ -238,7 +238,7 @@ describe('the plugin', function () {
"conditionId": "39584410-cbf9-499e-96dc-76f27e69885d",
"style": {
"isStyleInvisible": "",
"backgroundColor": "#717171",
"backgroundColor": "#666666",
"border": "1px solid #ffff00"
}
};
@@ -250,7 +250,7 @@ describe('the plugin', function () {
"configuration": {
"items": [
{
"fill": "#717171",
"fill": "#666666",
"stroke": "",
"x": 1,
"y": 1,
@@ -259,12 +259,22 @@ describe('the plugin', function () {
"type": "box-view",
"id": "89b88746-d325-487b-aec4-11b79afff9e8"
},
{
"fill": "#666666",
"stroke": "",
"x": 1,
"y": 1,
"width": 10,
"height": 5,
"type": "ellipse-view",
"id": "19b88746-d325-487b-aec4-11b79afff9z8"
},
{
"x": 18,
"y": 9,
"x2": 23,
"y2": 4,
"stroke": "#717171",
"stroke": "#666666",
"type": "line-view",
"id": "57d49a28-7863-43bd-9593-6570758916f0"
},
@@ -299,12 +309,12 @@ describe('the plugin', function () {
"y": 9,
"x2": 23,
"y2": 4,
"stroke": "#717171",
"stroke": "#666666",
"type": "line-view",
"id": "57d49a28-7863-43bd-9593-6570758916f0"
};
boxLayoutItem = {
"fill": "#717171",
"fill": "#666666",
"stroke": "",
"x": 1,
"y": 1,

View File

@@ -29,9 +29,10 @@ const styleProps = {
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view'
|| type === 'telemetry-view'
|| type === 'box-view'
|| type === 'subobject-view');
|| type === 'telemetry-view'
|| type === 'box-view'
|| type === 'ellipse-view'
|| type === 'subobject-view');
}
},
border: {
@@ -41,6 +42,7 @@ const styleProps = {
return !type ? true : (type === 'text-view'
|| type === 'telemetry-view'
|| type === 'box-view'
|| type === 'ellipse-view'
|| type === 'image-view'
|| type === 'line-view'
|| type === 'subobject-view');

View File

@@ -149,6 +149,7 @@ define(['lodash'], function (_) {
return type === 'text-view'
|| type === 'telemetry-view'
|| type === 'box-view'
|| type === 'ellipse-view'
|| type === 'image-view'
|| type === 'line-view'
|| type === 'subobject-view';
@@ -180,6 +181,10 @@ define(['lodash'], function (_) {
"name": "Box",
"class": "icon-box-round-corners"
},
{
"name": "Ellipse",
"class": "icon-circle"
},
{
"name": "Line",
"class": "icon-line-horz"
@@ -745,7 +750,7 @@ define(['lodash'], function (_) {
if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
}
} else if (layoutItem.type === 'box-view') {
} else if (layoutItem.type === 'box-view' || layoutItem.type === 'ellipse-view') {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),

View File

@@ -43,7 +43,7 @@ import conditionalStylesMixin from '../mixins/objectStyles-mixin';
export default {
makeDefinition() {
return {
fill: '#717171',
fill: '#666666',
stroke: '',
x: 1,
y: 1,

View File

@@ -76,6 +76,7 @@ import uuid from 'uuid';
import SubobjectView from './SubobjectView.vue';
import TelemetryView from './TelemetryView.vue';
import BoxView from './BoxView.vue';
import EllipseView from './EllipseView.vue';
import TextView from './TextView.vue';
import LineView from './LineView.vue';
import ImageView from './ImageView.vue';
@@ -112,6 +113,7 @@ const ITEM_TYPE_VIEW_MAP = {
'subobject-view': SubobjectView,
'telemetry-view': TelemetryView,
'box-view': BoxView,
'ellipse-view': EllipseView,
'line-view': LineView,
'text-view': TextView,
'image-view': ImageView

View File

@@ -28,19 +28,19 @@
>
<div
class="c-frame-edit__handle c-frame-edit__handle--nw"
@mousedown="startResize([1,1], [-1,-1], $event)"
@mousedown.left="startResize([1,1], [-1,-1], $event)"
></div>
<div
class="c-frame-edit__handle c-frame-edit__handle--ne"
@mousedown="startResize([0,1], [1,-1], $event)"
@mousedown.left="startResize([0,1], [1,-1], $event)"
></div>
<div
class="c-frame-edit__handle c-frame-edit__handle--sw"
@mousedown="startResize([1,0], [-1,1], $event)"
@mousedown.left="startResize([1,0], [-1,1], $event)"
></div>
<div
class="c-frame-edit__handle c-frame-edit__handle--se"
@mousedown="startResize([0,0], [1,1], $event)"
@mousedown.left="startResize([0,0], [1,1], $event)"
></div>
</div>
</template>

View File

@@ -0,0 +1,122 @@
/*****************************************************************************
* 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.
*****************************************************************************/
<template>
<layout-frame
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
<div
class="c-ellipse-view u-style-receiver js-style-receiver"
:class="[styleClass]"
:style="style"
></div>
</layout-frame>
</template>
<script>
import LayoutFrame from './LayoutFrame.vue';
import conditionalStylesMixin from '../mixins/objectStyles-mixin';
export default {
makeDefinition() {
return {
fill: '#666666',
stroke: '',
x: 1,
y: 1,
width: 10,
height: 10
};
},
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
inject: ['openmct'],
props: {
item: {
type: Object,
required: true
},
gridSize: {
type: Array,
required: true,
validator: (arr) => arr && arr.length === 2
&& arr.every(el => typeof el === 'number')
},
index: {
type: Number,
required: true
},
initSelect: Boolean,
isEditing: {
type: Boolean,
required: true
}
},
computed: {
style() {
if (this.itemStyle) {
return this.itemStyle;
} else {
return {
backgroundColor: this.item.fill,
border: this.item.stroke ? '1px solid ' + this.item.stroke : ''
};
}
}
},
watch: {
index(newIndex) {
if (!this.context) {
return;
}
this.context.index = newIndex;
},
item(newItem) {
if (!this.context) {
return;
}
this.context.layoutItem = newItem;
}
},
mounted() {
this.context = {
layoutItem: this.item,
index: this.index
};
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.initSelect);
},
destroyed() {
if (this.removeSelectable) {
this.removeSelectable();
}
}
};
</script>

View File

@@ -33,7 +33,7 @@
<slot></slot>
<div
class="c-frame__move-bar"
@mousedown="isEditing ? startMove([1,1], [0,0], $event) : null"
@mousedown.left="startMove($event)"
></div>
</div>
</template>
@@ -93,7 +93,11 @@ export default {
return value - this.initialPosition[index];
}.bind(this));
},
startMove(posFactor, dimFactor, event) {
startMove(event, posFactor = [1, 1], dimFactor = [0, 0]) {
if (!this.isEditing) {
return;
}
document.body.addEventListener('mousemove', this.continueMove);
document.body.addEventListener('mouseup', this.endMove);
this.dragPosition = {

View File

@@ -45,7 +45,7 @@
<div
class="c-frame__move-bar"
@mousedown="startDrag($event)"
@mousedown.left="startDrag($event)"
></div>
<div
v-if="showFrameEdit"
@@ -96,7 +96,7 @@ export default {
y: 10,
x2: 10,
y2: 5,
stroke: '#717171'
stroke: '#666666'
};
},
mixins: [conditionalStylesMixin],

View File

@@ -72,7 +72,7 @@
<script>
import LayoutFrame from './LayoutFrame.vue';
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
import { getDefaultNotebook, getNotebookSectionAndPage } from '@/plugins/notebook/utils/notebook-storage.js';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1];
@@ -336,12 +336,15 @@ export default {
},
async getContextMenuActions() {
const defaultNotebook = getDefaultNotebook();
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
let defaultNotebookName;
if (defaultNotebook) {
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
defaultNotebookName = `Copy to Notebook ${defaultPath}`;
const domainObject = await this.openmct.objects.get(defaultNotebook.identifier);
const { section, page } = getNotebookSectionAndPage(domainObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId);
if (section && page) {
const defaultPath = domainObject && `${domainObject.name} - ${section.name} - ${page.name}`;
defaultNotebookName = `Copy to Notebook ${defaultPath}`;
}
}
return CONTEXT_MENU_ACTIONS

View File

@@ -1,4 +1,5 @@
.c-box-view {
.c-box-view,
.c-ellipse-view {
border-width: $drawingObjBorderW !important;
display: flex;
align-items: stretch;
@@ -8,6 +9,10 @@
}
}
.c-ellipse-view {
border-radius: 50%;
}
.c-line-view {
&.c-frame {
box-shadow: none !important;

View File

@@ -186,7 +186,7 @@ describe('the plugin', function () {
'configuration': {
'items': [
{
'fill': '#717171',
'fill': '#666666',
'stroke': '',
'x': 1,
'y': 1,
@@ -195,12 +195,22 @@ describe('the plugin', function () {
'type': 'box-view',
'id': '89b88746-d325-487b-aec4-11b79afff9e8'
},
{
'fill': '#666666',
'stroke': '',
'x': 1,
'y': 1,
'width': 10,
'height': 10,
'type': 'ellipse-view',
'id': '19b88746-d325-487b-aec4-11b79afff9z8'
},
{
'x': 18,
'y': 9,
'x2': 23,
'y2': 4,
'stroke': '#717171',
'stroke': '#666666',
'type': 'line-view',
'id': '57d49a28-7863-43bd-9593-6570758916f0'
},
@@ -341,7 +351,7 @@ describe('the plugin', function () {
it('provides controls including separators', () => {
const displayLayoutToolbar = openmct.toolbars.get(selection);
expect(displayLayoutToolbar.length).toBe(9);
expect(displayLayoutToolbar.length).toBe(7);
});
});
});

View File

@@ -33,10 +33,6 @@ export default function HyperlinkProvider(openmct) {
return domainObject.type === 'hyperlink';
},
canEdit(domainObject) {
return domainObject.type === 'hyperlink';
},
view: function (domainObject) {
let component;

View File

@@ -1,4 +1,4 @@
import { getDefaultNotebook } from '../utils/notebook-storage';
import { getDefaultNotebook, getNotebookSectionAndPage } from '../utils/notebook-storage';
import { addNotebookEntry } from '../utils/notebook-entries';
export default class CopyToNotebookAction {
@@ -15,11 +15,16 @@ export default class CopyToNotebookAction {
copyToNotebook(entryText) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
this.openmct.objects.get(notebookStorage.identifier)
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText);
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const { section, page } = getNotebookSectionAndPage(domainObject, notebookStorage.defaultSectionId, notebookStorage.defaultPageId);
if (!section || !page) {
return;
}
const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this.openmct.notifications.info(msg);
});

View File

@@ -43,14 +43,16 @@
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:default-page-id="defaultPageId"
:selected-page-id="selectedPageId"
:selected-page-id="getSelectedPageId()"
:default-section-id="defaultSectionId"
:selected-section-id="selectedSectionId"
:selected-section-id="getSelectedSectionId()"
:domain-object="domainObject"
:page-title="domainObject.configuration.pageTitle"
:section-title="domainObject.configuration.sectionTitle"
:sections="sections"
:sidebar-covers-entries="sidebarCoversEntries"
@defaultPageDeleted="cleanupDefaultNotebook"
@defaultSectionDeleted="cleanupDefaultNotebook"
@pagesChanged="pagesChanged"
@selectPage="selectPage"
@sectionsChanged="sectionsChanged"
@@ -136,8 +138,9 @@ import NotebookEntry from './NotebookEntry.vue';
import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
import objectUtils from 'objectUtils';
@@ -164,8 +167,10 @@ export default {
},
data() {
return {
selectedSectionId: this.getDefaultSectionId(),
selectedPageId: this.getDefaultPageId(),
defaultPageId: this.getDefaultPageId(),
defaultSectionId: this.getDefaultSectionId(),
selectedSectionId: this.getSelectedSectionId(),
selectedPageId: this.getSelectedPageId(),
defaultSort: this.domainObject.configuration.defaultSort,
focusEntryId: null,
search: '',
@@ -176,12 +181,6 @@ export default {
};
},
computed: {
defaultPageId() {
return this.getDefaultPageId();
},
defaultSectionId() {
return this.getDefaultSectionId();
},
filteredAndSortedEntries() {
const filterTime = Date.now();
const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
@@ -203,24 +202,38 @@ export default {
},
selectedPage() {
const pages = this.getPages();
const selectedPage = pages.find(page => page.id === this.selectedPageId);
if (!pages.length) {
return undefined;
}
const selectedPage = pages.find(page => page.id === this.selectedPageId);
if (selectedPage) {
return selectedPage;
}
if (!selectedPage && !pages.length) {
return undefined;
const defaultPage = pages.find(page => page.id === this.defaultPageId);
if (defaultPage) {
return defaultPage;
}
return pages[0];
return this.pages[0];
},
selectedSection() {
if (!this.sections.length) {
return null;
return undefined;
}
return this.sections.find(section => section.id === this.selectedSectionId);
const selectedSection = this.sections.find(section => section.id === this.selectedSectionId);
if (selectedSection) {
return selectedSection;
}
const defaultSection = this.sections.find(section => section.id === this.defaultSectionId);
if (defaultSection) {
return defaultSection;
}
return this.sections[0];
}
},
watch: {
@@ -301,26 +314,29 @@ export default {
this.sectionsChanged({ sections });
this.resetSearch();
},
cleanupDefaultNotebook() {
this.defaultPageId = undefined;
this.defaultSectionId = undefined;
this.removeDefaultClass(this.domainObject);
clearDefaultNotebook();
},
setSectionAndPageFromUrl() {
let sectionId = this.getSectionIdFromUrl() || this.selectedSectionId;
let pageId = this.getPageIdFromUrl() || this.selectedPageId;
let sectionId = this.getSectionIdFromUrl() || this.getDefaultSectionId() || this.getSelectedSectionId();
let pageId = this.getPageIdFromUrl() || this.getDefaultPageId() || this.getSelectedPageId();
this.selectSection(sectionId);
this.selectPage(pageId);
},
createNotebookStorageObject() {
const notebookMeta = {
name: this.domainObject.name,
identifier: this.domainObject.identifier,
link: this.getLinktoNotebook()
};
const page = this.selectedPage;
const section = this.selectedSection;
return {
notebookMeta,
page,
section
name: this.domainObject.name,
identifier: this.domainObject.identifier,
link: this.getLinktoNotebook(),
defaultSectionId: section.id,
defaultPageId: page.id
};
},
deleteEntry(entryId) {
@@ -370,9 +386,13 @@ export default {
const snapshotId = event.dataTransfer.getData('openmct/snapshot/id');
if (snapshotId.length) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.newEntry(snapshot);
this.newEntry(snapshot.embedObject);
this.snapshotContainer.removeSnapshot(snapshotId);
const namespace = this.domainObject.identifier.namespace;
const notebookImageDomainObject = updateNamespaceOfDomainObject(snapshot.notebookImageDomainObject, namespace);
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
return;
}
@@ -419,43 +439,26 @@ export default {
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;
return this.isDefaultNotebook()
? getDefaultNotebook().defaultPageId
: undefined;
},
isDefaultNotebook() {
const defaultNotebook = getDefaultNotebook();
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.notebookMeta.identifier;
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.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;
return this.isDefaultNotebook()
? getDefaultNotebook().defaultSectionId
: undefined;
},
getDefaultNotebookObject() {
const oldNotebookStorage = getDefaultNotebook();
if (!oldNotebookStorage) {
return null;
}
const defaultNotebook = getDefaultNotebook();
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier);
return defaultNotebook && this.openmct.objects.get(defaultNotebook.identifier);
},
getLinktoNotebook() {
const objectPath = this.openmct.router.path;
@@ -573,6 +576,22 @@ export default {
return selectedSection.pages;
},
getSelectedPageId() {
const page = this.selectedPage;
if (!page) {
return undefined;
}
return page.id;
},
getSelectedSectionId() {
const section = this.selectedSection;
if (!section) {
return undefined;
}
return section.id;
},
newEntry(embed = null) {
this.resetSearch();
const notebookStorage = this.createNotebookStorageObject();
@@ -616,51 +635,26 @@ export default {
},
async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
if (!defaultNotebookObject) {
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
const isSameNotebook = defaultNotebookObject
&& objectUtils.makeKeyString(defaultNotebookObject.identifier) === objectUtils.makeKeyString(notebookStorage.identifier);
if (!isSameNotebook) {
this.removeDefaultClass(defaultNotebookObject);
}
if (!defaultNotebookObject || !isSameNotebook) {
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
}
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
this.defaultSectionId = notebookStorage.section.id;
setDefaultNotebookSection(notebookStorage.section);
if (this.defaultSectionId !== notebookStorage.defaultSectionId) {
setDefaultNotebookSectionId(notebookStorage.defaultSectionId);
this.defaultSectionId = notebookStorage.defaultSectionId;
}
if (this.defaultPageId && this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
this.defaultPageId = notebookStorage.page.id;
setDefaultNotebookPage(notebookStorage.page);
if (this.defaultPageId !== notebookStorage.defaultPageId) {
setDefaultNotebookPageId(notebookStorage.defaultPageId);
this.defaultPageId = notebookStorage.defaultPageId;
}
},
updateDefaultNotebookPage(pages, id) {
if (!id) {
return;
}
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
return;
}
const defaultNotebookPage = notebookStorage.page;
const page = pages.find(p => p.id === id);
if (!page && defaultNotebookPage.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null;
this.removeDefaultClass(this.domainObject);
clearDefaultNotebook();
return;
}
if (id !== defaultNotebookPage.id) {
return;
}
setDefaultNotebookPage(page);
},
updateDefaultNotebookSection(sections, id) {
if (!id) {
return;
@@ -668,26 +662,26 @@ export default {
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
|| notebookStorage.identifier.key !== this.domainObject.identifier.key) {
return;
}
const defaultNotebookSection = notebookStorage.section;
const section = sections.find(s => s.id === id);
if (!section && defaultNotebookSection.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null;
this.removeDefaultClass(this.domainObject);
clearDefaultNotebook();
const defaultNotebookSectionId = notebookStorage.defaultSectionId;
if (defaultNotebookSectionId === id) {
const section = sections.find(s => s.id === id);
if (!section) {
this.removeDefaultClass(this.domainObject);
clearDefaultNotebook();
return;
}
}
if (id !== defaultNotebookSectionId) {
return;
}
if (id !== defaultNotebookSection.id) {
return;
}
setDefaultNotebookSection(section);
setDefaultNotebookSectionId(defaultNotebookSectionId);
},
updateEntry(entry) {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
@@ -715,19 +709,27 @@ export default {
sectionId: this.selectedSectionId
});
},
sectionsChanged({ sections, id = null }) {
sectionsChanged({ sections, id = undefined }) {
mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections);
this.updateDefaultNotebookSection(sections, id);
},
selectPage(pageId) {
if (!pageId) {
return;
}
this.selectedPageId = pageId;
this.syncUrlWithPageAndSection();
},
selectSection(sectionId) {
if (!sectionId) {
return;
}
this.selectedSectionId = sectionId;
const defaultPageId = this.selectedSection.pages[0].id;
this.selectPage(defaultPageId);
const pageId = this.selectedSection.pages[0].id;
this.selectPage(pageId);
this.syncUrlWithPageAndSection();
}

View File

@@ -40,7 +40,7 @@ export default {
components: {
PopupMenu
},
inject: ['openmct'],
inject: ['openmct', 'snapshotContainer'],
props: {
embed: {
type: Object,
@@ -48,6 +48,12 @@ export default {
return {};
}
},
isSnapshotContainer: {
type: Boolean,
default() {
return false;
}
},
removeActionString: {
type: String,
default() {
@@ -135,6 +141,14 @@ export default {
return;
}
if (this.isSnapshotContainer) {
const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);
const fullSizeImageURL = snapshot.notebookImageDomainObject.configuration.fullSizeImageURL;
painterroInstance.show(fullSizeImageURL);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
painterroInstance.show(object.configuration.fullSizeImageURL);
@@ -190,6 +204,14 @@ export default {
return;
}
if (this.isSnapshotContainer) {
const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);
const fullSizeImageURL = snapshot.notebookImageDomainObject.configuration.fullSizeImageURL;
this.openSnapshotOverlay(fullSizeImageURL);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
this.openSnapshotOverlay(object.configuration.fullSizeImageURL);
@@ -259,8 +281,20 @@ export default {
updateSnapshot(snapshotObject) {
this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
this.updateNotebookImageDomainObjectSnapshot(snapshotObject);
this.updateEmbed(this.embed);
},
updateNotebookImageDomainObjectSnapshot(snapshotObject) {
if (this.isSnapshotContainer) {
const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);
snapshot.embedObject.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
snapshot.notebookImageDomainObject.configuration.fullSizeImageURL = snapshotObject.fullSizeImage.src;
this.snapshotContainer.updateSnapshot(snapshot);
} else {
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
}
}
}
};

View File

@@ -102,9 +102,11 @@
<script>
import NotebookEmbed from './NotebookEmbed.vue';
import { createNewEmbed } from '../utils/notebook-entries';
import Moment from 'moment';
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
import { createNewEmbed } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import Moment from 'moment';
export default {
components: {
@@ -210,8 +212,12 @@ export default {
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
if (snapshotId.length) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.entry.embeds.push(snapshot.embedObject);
this.snapshotContainer.removeSnapshot(snapshotId);
this.entry.embeds.push(snapshot);
const namespace = this.domainObject.identifier.namespace;
const notebookImageDomainObject = updateNamespaceOfDomainObject(snapshot.notebookImageDomainObject, namespace);
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
} else {
const data = $event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);

View File

@@ -19,17 +19,24 @@
import Snapshot from '../snapshot';
import { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage';
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
import { getMenuItems } from '../utils/notebook-snapshot-menu';
export default {
inject: ['openmct'],
props: {
currentView: {
type: Object,
default() {
return {};
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
ignoreLink: {
isPreview: {
type: Boolean,
default() {
return false;
@@ -50,65 +57,53 @@ export default {
},
mounted() {
validateNotebookStorageObject();
this.getDefaultNotebookObject();
this.notebookSnapshot = new Snapshot(this.openmct);
this.setDefaultNotebookStatus();
},
methods: {
async getDefaultNotebookObject() {
const defaultNotebook = getDefaultNotebook();
const defaultNotebookObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
getPreviewObjectLink() {
const relativePath = this.openmct.objects.getRelativePath(this.objectPath);
const urlParams = this.openmct.router.getParams();
urlParams.view = this.currentView.key;
return defaultNotebookObject;
const urlParamsString = Object.entries(urlParams)
.map(([key, value]) => `${key}=${value}`)
.join('&');
return `#/browse/${relativePath}?${urlParamsString}`;
},
async showMenu(event) {
const notebookTypes = [];
const menuItemOptions = {
default: {
cssClass: 'icon-notebook',
name: `Save to Notebook`,
onItemClicked: () => this.snapshot(NOTEBOOK_DEFAULT, event.target)
},
snapshot: {
cssClass: 'icon-camera',
name: 'Save to Notebook Snapshots',
onItemClicked: () => this.snapshot(NOTEBOOK_SNAPSHOT, event.target)
}
};
const notebookTypes = await getMenuItems(this.openmct, menuItemOptions);
const elementBoundingClientRect = this.$el.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
const defaultNotebookObject = await this.getDefaultNotebookObject();
if (defaultNotebookObject) {
const name = defaultNotebookObject.name;
const defaultNotebook = getDefaultNotebook();
const sectionName = defaultNotebook.section.name;
const pageName = defaultNotebook.page.name;
const defaultPath = `${name} - ${sectionName} - ${pageName}`;
notebookTypes.push({
cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`,
onItemClicked: () => {
return this.snapshot(NOTEBOOK_DEFAULT);
}
});
}
notebookTypes.push({
cssClass: 'icon-camera',
name: 'Save to Notebook Snapshots',
onItemClicked: () => {
return this.snapshot(NOTEBOOK_SNAPSHOT);
}
});
this.openmct.menus.showMenu(x, y, notebookTypes);
},
snapshot(notebookType) {
snapshot(notebookType, target) {
this.$nextTick(() => {
const element = document.querySelector('.c-overlay__contents')
|| document.getElementsByClassName('l-shell__main-container')[0];
const bounds = this.openmct.time.bounds();
const link = !this.ignoreLink
? window.location.hash
: null;
const wrapper = target && target.closest('.js-notebook-snapshot-item-wrapper')
|| document;
const element = wrapper.querySelector('.js-notebook-snapshot-item');
const objectPath = this.objectPath || this.openmct.router.path;
const link = this.isPreview
? this.getPreviewObjectLink()
: window.location.hash;
const snapshotMeta = {
bounds,
bounds: this.openmct.time.bounds(),
link,
objectPath,
openmct: this.openmct
@@ -119,9 +114,8 @@ export default {
},
setDefaultNotebookStatus() {
let defaultNotebookObject = getDefaultNotebook();
if (defaultNotebookObject && defaultNotebookObject.notebookMeta) {
let notebookIdentifier = defaultNotebookObject.notebookMeta.identifier;
if (defaultNotebookObject) {
let notebookIdentifier = defaultNotebookObject.identifier;
this.openmct.status.set(notebookIdentifier, 'notebook-default');
}

View File

@@ -27,15 +27,15 @@
</div><!-- closes l-browse-bar -->
<div class="c-snapshots">
<span v-for="snapshot in snapshots"
:key="snapshot.id"
:key="snapshot.embedObject.id"
draggable="true"
@dragstart="startEmbedDrag(snapshot, $event)"
>
<NotebookEmbed ref="notebookEmbed"
:key="snapshot.id"
:embed="snapshot"
:key="snapshot.embedObject.id"
:embed="snapshot.embedObject"
:is-snapshot-container="true"
:remove-action-string="'Delete Snapshot'"
@updateEmbed="updateSnapshot"
@removeEmbed="removeSnapshot"
/>
</span>
@@ -119,11 +119,8 @@ export default {
this.snapshots = this.snapshotContainer.getSnapshots();
},
startEmbedDrag(snapshot, event) {
event.dataTransfer.setData('text/plain', snapshot.id);
event.dataTransfer.setData('openmct/snapshot/id', snapshot.id);
},
updateSnapshot(snapshot) {
this.snapshotContainer.updateSnapshot(snapshot);
event.dataTransfer.setData('text/plain', snapshot.embedObject.id);
event.dataTransfer.setData('openmct/snapshot/id', snapshot.embedObject.id);
}
}
};

View File

@@ -87,22 +87,26 @@ export default {
const selectedPage = this.pages.find(p => p.isSelected);
const defaultNotebook = getDefaultNotebook();
const defaultpage = defaultNotebook && defaultNotebook.page;
const defaultPageId = defaultNotebook && defaultNotebook.defaultPageId;
const isPageSelected = selectedPage && selectedPage.id === id;
const isPageDefault = defaultpage && defaultpage.id === id;
const isPageDefault = defaultPageId === id;
const pages = this.pages.filter(s => s.id !== id);
let selectedPageId;
if (isPageSelected && defaultpage) {
if (isPageSelected && defaultPageId) {
pages.forEach(s => {
s.isSelected = false;
if (defaultpage && defaultpage.id === s.id) {
if (defaultPageId === s.id) {
selectedPageId = s.id;
}
});
}
if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) {
if (isPageDefault) {
this.$emit('defaultPageDeleted');
}
if (pages.length && isPageSelected && (!defaultPageId || isPageDefault)) {
selectedPageId = pages[0].id;
}

View File

@@ -75,21 +75,25 @@ export default {
const selectedSection = this.sections.find(s => s.id === this.selectedSectionId);
const defaultNotebook = getDefaultNotebook();
const defaultSection = defaultNotebook && defaultNotebook.section;
const defaultSectionId = defaultNotebook && defaultNotebook.defaultSectionId;
const isSectionSelected = selectedSection && selectedSection.id === id;
const isSectionDefault = defaultSection && defaultSection.id === id;
const isSectionDefault = defaultSectionId === id;
const sections = this.sections.filter(s => s.id !== id);
if (isSectionSelected && defaultSection) {
if (isSectionSelected && defaultSectionId) {
sections.forEach(s => {
s.isSelected = false;
if (defaultSection && defaultSection.id === s.id) {
if (defaultSectionId === s.id) {
s.isSelected = true;
}
});
}
if (sections.length && isSectionSelected && (!defaultSection || isSectionDefault)) {
if (isSectionDefault) {
this.$emit('defaultSectionDeleted');
}
if (sections.length && isSectionSelected && (!defaultSectionId || isSectionDefault)) {
sections[0].isSelected = true;
}

View File

@@ -19,6 +19,7 @@
:domain-object="domainObject"
:sections="sections"
:section-title="sectionTitle"
@defaultSectionDeleted="defaultSectionDeleted"
@updateSection="sectionsChanged"
@selectSection="selectSection"
/>
@@ -50,6 +51,7 @@
:sections="sections"
:sidebar-covers-entries="sidebarCoversEntries"
:page-title="pageTitle"
@defaultPageDeleted="defaultPageDeleted"
@toggleNav="toggleNav"
@updatePage="pagesChanged"
@selectPage="selectPage"
@@ -218,6 +220,12 @@ export default {
sectionTitle
};
},
defaultPageDeleted() {
this.$emit('defaultPageDeleted');
},
defaultSectionDeleted() {
this.$emit('defaultSectionDeleted');
},
toggleNav() {
this.$emit('toggleNav');
},

View File

@@ -18,13 +18,18 @@ export default class SnapshotContainer extends EventEmitter {
return SnapshotContainer.instance;
}
addSnapshot(embedObject) {
addSnapshot(notebookImageDomainObject, embedObject) {
const snapshots = this.getSnapshots();
if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) {
snapshots.pop();
}
snapshots.unshift(embedObject);
const snapshotObject = {
notebookImageDomainObject,
embedObject
};
snapshots.unshift(snapshotObject);
return this.saveSnapshots(snapshots);
}
@@ -32,7 +37,7 @@ export default class SnapshotContainer extends EventEmitter {
getSnapshot(id) {
const snapshots = this.getSnapshots();
return snapshots.find(s => s.id === id);
return snapshots.find(s => s.embedObject.id === id);
}
getSnapshots() {
@@ -47,7 +52,7 @@ export default class SnapshotContainer extends EventEmitter {
}
const snapshots = this.getSnapshots();
const filteredsnapshots = snapshots.filter(snapshot => snapshot.id !== id);
const filteredsnapshots = snapshots.filter(snapshot => snapshot.embedObject.id !== id);
return this.saveSnapshots(filteredsnapshots);
}
@@ -73,7 +78,7 @@ export default class SnapshotContainer extends EventEmitter {
updateSnapshot(snapshot) {
const snapshots = this.getSnapshots();
const updatedSnapshots = snapshots.map(s => {
return s.id === snapshot.id
return s.embedObject.id === snapshot.embedObject.id
? snapshot
: s;
});

View File

@@ -1,7 +1,7 @@
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
import { getDefaultNotebook, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
import { getDefaultNotebook, getNotebookSectionAndPage, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
import { createNotebookImageDomainObject, saveNotebookImageDomainObject, updateNamespaceOfDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
import SnapshotContainer from './snapshot-container';
import ImageExporter from '../../exporters/ImageExporter';
@@ -35,43 +35,47 @@ export default class Snapshot {
* @private
*/
_saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) {
createNotebookImageDomainObject(this.openmct, fullSizeImageURL)
.then(object => {
const thumbnailImage = { src: thumbnailImageURL || '' };
const snapshot = {
fullSizeImageObjectIdentifier: object.identifier,
thumbnailImage
};
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
this._saveToDefaultNoteBook(embed);
const object = createNotebookImageDomainObject(fullSizeImageURL);
const thumbnailImage = { src: thumbnailImageURL || '' };
const snapshot = {
fullSizeImageObjectIdentifier: object.identifier,
thumbnailImage
};
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
const notebookStorage = getDefaultNotebook();
return;
}
this._saveToNotebookSnapshots(embed);
});
this._saveToDefaultNoteBook(notebookStorage, embed);
const notebookImageDomainObject = updateNamespaceOfDomainObject(object, notebookStorage.identifier.namespace);
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
} else {
this._saveToNotebookSnapshots(object, embed);
}
}
/**
* @private
*/
_saveToDefaultNoteBook(embed) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
_saveToDefaultNoteBook(notebookStorage, embed) {
this.openmct.objects.get(notebookStorage.identifier)
.then(async (domainObject) => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
let link = notebookStorage.notebookMeta.link;
let link = notebookStorage.link;
// Backwards compatibility fix (old notebook model without link)
if (!link) {
link = await getDefaultNotebookLink(this.openmct, domainObject);
notebookStorage.notebookMeta.link = link;
notebookStorage.link = link;
setDefaultNotebook(this.openmct, notebookStorage);
}
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const { section, page } = getNotebookSectionAndPage(domainObject, notebookStorage.defaultSectionId, notebookStorage.defaultPageId);
if (!section || !page) {
return;
}
const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this._showNotification(msg, link);
});
@@ -80,19 +84,22 @@ export default class Snapshot {
/**
* @private
*/
_saveToNotebookSnapshots(embed) {
this.snapshotContainer.addSnapshot(embed);
_saveToNotebookSnapshots(notebookImageDomainObject, embed) {
this.snapshotContainer.addSnapshot(notebookImageDomainObject, embed);
}
_showNotification(msg, url) {
const options = {
autoDismissTimeout: 30000,
link: {
autoDismissTimeout: 30000
};
if (!this.openmct.editor.isEditing()) {
options.link = {
cssClass: '',
text: 'click to view',
onClick: this._navigateToNotebook(url)
}
};
};
}
this.openmct.notifications.info(msg, options);
}

View File

@@ -9,24 +9,24 @@ const TIME_BOUNDS = {
};
export function addEntryIntoPage(notebookStorage, entries, entry) {
const defaultSection = notebookStorage.section;
const defaultPage = notebookStorage.page;
if (!defaultSection || !defaultPage) {
const defaultSectionId = notebookStorage.defaultSectionId;
const defaultPageId = notebookStorage.defaultPageId;
if (!defaultSectionId || !defaultPageId) {
return;
}
const newEntries = JSON.parse(JSON.stringify(entries));
let section = newEntries[defaultSection.id];
let section = newEntries[defaultSectionId];
if (!section) {
newEntries[defaultSection.id] = {};
newEntries[defaultSectionId] = {};
}
let page = newEntries[defaultSection.id][defaultPage.id];
let page = newEntries[defaultSectionId][defaultPageId];
if (!page) {
newEntries[defaultSection.id][defaultPage.id] = [];
newEntries[defaultSectionId][defaultPageId] = [];
}
newEntries[defaultSection.id][defaultPage.id].push(entry);
newEntries[defaultSectionId][defaultPageId].push(entry);
return newEntries;
}

View File

@@ -23,28 +23,13 @@ import * as NotebookEntries from './notebook-entries';
import { createOpenMct, resetApplicationState } from 'utils/testing';
const notebookStorage = {
notebookMeta: {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
}
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
section: {
id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
isDefault: true,
isSelected: true,
name: 'section',
pages: [],
sectionTitle: 'Section'
},
page: {
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
isDefault: true,
isSelected: true,
name: 'page',
pageTitle: 'Page'
}
defaultSectionId: '03a79b6a-971c-4e56-9892-ec536332c3f0',
defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00'
};
const notebookEntries = {

View File

@@ -5,14 +5,14 @@ export const DEFAULT_SIZE = {
height: 30
};
export function createNotebookImageDomainObject(openmct, fullSizeImageURL) {
export function createNotebookImageDomainObject(fullSizeImageURL) {
const identifier = {
key: uuid(),
namespace: ''
};
const viewType = 'notebookSnapshotImage';
const object = {
return {
name: 'Notebook Snapshot Image',
type: viewType,
identifier,
@@ -20,21 +20,6 @@ export function createNotebookImageDomainObject(openmct, fullSizeImageURL) {
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) {
@@ -67,6 +52,23 @@ export function getThumbnailURLFromimageUrl(imageUrl, size = DEFAULT_SIZE) {
});
}
export function saveNotebookImageDomainObject(openmct, object) {
return new Promise((resolve, reject) => {
openmct.objects.save(object)
.then(result => {
if (result) {
resolve(object);
} else {
reject();
}
})
.catch(e => {
console.error(e);
reject();
});
});
}
export function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) {
openmct.objects.get(identifier)
.then(domainObject => {
@@ -76,3 +78,9 @@ export function updateNotebookImageDomainObject(openmct, identifier, fullSizeIma
openmct.objects.mutate(domainObject, 'configuration', configuration);
});
}
export function updateNamespaceOfDomainObject(object, namespace) {
object.identifier.namespace = namespace;
return object;
}

View File

@@ -1,16 +1,18 @@
import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl } from './notebook-image';
import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl, saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from './notebook-image';
import { mutateObject } from './notebook-entries';
const IMAGE_MIGRATION_VER = "v1";
export function notebookImageMigration(openmct, domainObject) {
const configuration = domainObject.configuration;
const notebookEntries = configuration.entries;
const imageMigrationVer = configuration.imageMigrationVer;
if (imageMigrationVer && imageMigrationVer === 'v1') {
if (imageMigrationVer && imageMigrationVer === IMAGE_MIGRATION_VER) {
return;
}
configuration.imageMigrationVer = 'v1';
configuration.imageMigrationVer = IMAGE_MIGRATION_VER;
// to avoid muliple notebookImageMigration calls updating images.
mutateObject(openmct, domainObject, 'configuration', configuration);
@@ -27,14 +29,16 @@ export function notebookImageMigration(openmct, domainObject) {
const fullSizeImageURL = snapshot.src;
if (fullSizeImageURL) {
const thumbnailImageURL = await getThumbnailURLFromimageUrl(fullSizeImageURL);
const notebookImageDomainObject = await createNotebookImageDomainObject(openmct, fullSizeImageURL);
const object = createNotebookImageDomainObject(fullSizeImageURL);
const notebookImageDomainObject = updateNamespaceOfDomainObject(object, domainObject.identifier.namespace);
embed.snapshot = {
fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier,
thumbnailImage: { src: thumbnailImageURL || '' }
};
mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries);
saveNotebookImageDomainObject(openmct, notebookImageDomainObject);
}
});
});

View File

@@ -0,0 +1,31 @@
import { getDefaultNotebook, getNotebookSectionAndPage } from './notebook-storage';
export async function getMenuItems(openmct, menuItemOptions) {
const notebookTypes = [];
const defaultNotebook = getDefaultNotebook();
const defaultNotebookObject = defaultNotebook && await openmct.objects.get(defaultNotebook.identifier);
if (defaultNotebookObject) {
const { section, page } = getNotebookSectionAndPage(defaultNotebookObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId);
if (section && page) {
const name = defaultNotebookObject.name;
const sectionName = section.name;
const pageName = page.name;
const defaultPath = `${name} - ${sectionName} - ${pageName}`;
notebookTypes.push({
cssClass: menuItemOptions.default.cssClass,
name: `${menuItemOptions.default.name} ${defaultPath}`,
onItemClicked: menuItemOptions.default.onItemClicked
});
}
}
notebookTypes.push({
cssClass: menuItemOptions.snapshot.cssClass,
name: menuItemOptions.snapshot.name,
onItemClicked: menuItemOptions.snapshot.onItemClicked
});
return notebookTypes;
}

View File

@@ -19,18 +19,22 @@ function defaultNotebookObjectChanged(newDomainObject) {
clearDefaultNotebook();
}
function observeDefaultNotebookObject(openmct, notebookMeta, domainObject) {
function observeDefaultNotebookObject(openmct, notebookStorage, domainObject) {
if (currentNotebookObjectIdentifier
&& objectUtils.makeKeyString(currentNotebookObjectIdentifier) === objectUtils.makeKeyString(notebookMeta.identifier)) {
&& objectUtils.makeKeyString(currentNotebookObjectIdentifier) === objectUtils.makeKeyString(notebookStorage.identifier)) {
return;
}
removeListener();
unlisten = openmct.objects.observe(domainObject, '*', defaultNotebookObjectChanged);
}
function removeListener() {
if (unlisten) {
unlisten();
unlisten = null;
}
unlisten = openmct.objects.observe(domainObject, '*', defaultNotebookObjectChanged);
}
function saveDefaultNotebook(notebookStorage) {
@@ -39,6 +43,8 @@ function saveDefaultNotebook(notebookStorage) {
export function clearDefaultNotebook() {
currentNotebookObjectIdentifier = null;
removeListener();
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, null);
}
@@ -48,20 +54,27 @@ export function getDefaultNotebook() {
return JSON.parse(notebookStorage);
}
export function getNotebookSectionAndPage(domainObject, sectionId, pageId) {
const configuration = domainObject.configuration;
const section = configuration && configuration.sections.find(s => s.id === sectionId);
const page = section && section.pages.find(p => p.id === pageId);
return {
section,
page
};
}
export async function getDefaultNotebookLink(openmct, domainObject = null) {
if (!domainObject) {
return null;
}
const path = await openmct.objects.getOriginalPath(domainObject.identifier)
.then(objectPath => objectPath
.map(o => o && openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/')
);
const { page, section } = getDefaultNotebook();
.then(openmct.objects.getRelativePath);
const { defaultPageId, defaultSectionId } = getDefaultNotebook();
return `#/browse/${path}?sectionId=${section.id}&pageId=${page.id}`;
return `#/browse/${path}?sectionId=${defaultSectionId}&pageId=${defaultPageId}`;
}
export function setDefaultNotebook(openmct, notebookStorage, domainObject) {
@@ -69,15 +82,15 @@ export function setDefaultNotebook(openmct, notebookStorage, domainObject) {
saveDefaultNotebook(notebookStorage);
}
export function setDefaultNotebookSection(section) {
export function setDefaultNotebookSectionId(sectionId) {
const notebookStorage = getDefaultNotebook();
notebookStorage.section = section;
notebookStorage.defaultSectionId = sectionId;
saveDefaultNotebook(notebookStorage);
}
export function setDefaultNotebookPage(page) {
export function setDefaultNotebookPageId(pageId) {
const notebookStorage = getDefaultNotebook();
notebookStorage.page = page;
notebookStorage.defaultPageId = pageId;
saveDefaultNotebook(notebookStorage);
}
@@ -86,10 +99,13 @@ export function validateNotebookStorageObject() {
let valid = false;
if (notebookStorage) {
Object.entries(notebookStorage).forEach(([key, value]) => {
const oldInvalidKeys = ['notebookMeta', 'page', 'section'];
valid = Object.entries(notebookStorage).every(([key, value]) => {
const validKey = key !== undefined && key !== null;
const validValue = value !== undefined && value !== null;
valid = validKey && validValue;
const hasOldInvalidKeys = oldInvalidKeys.includes(key);
return validKey && validValue && !hasOldInvalidKeys;
});
}

View File

@@ -23,37 +23,44 @@
import * as NotebookStorage from './notebook-storage';
import { createOpenMct, resetApplicationState } from 'utils/testing';
const notebookSection = {
id: 'temp-section',
isDefault: false,
isSelected: true,
name: 'section',
pages: [
{
id: 'temp-page',
isDefault: false,
isSelected: true,
name: 'page',
pageTitle: 'Page'
}
],
sectionTitle: 'Section'
};
const domainObject = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
configuration: {
sections: [
notebookSection
]
}
};
const notebookStorage = {
notebookMeta: {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
}
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
section: {
id: 'temp-section',
isDefault: false,
isSelected: true,
name: 'section',
pages: [],
sectionTitle: 'Section'
},
page: {
id: 'temp-page',
isDefault: false,
isSelected: true,
name: 'page',
pageTitle: 'Page'
}
defaultSectionId: 'temp-section',
defaultPageId: 'temp-page'
};
let openmct;
@@ -104,7 +111,7 @@ describe('Notebook Storage:', () => {
expect(JSON.stringify(defaultNotebook)).toBe(JSON.stringify(notebookStorage));
});
it('has correct section on setDefaultNotebookSection', () => {
it('has correct section on setDefaultNotebookSectionId', () => {
const section = {
id: 'new-temp-section',
isDefault: true,
@@ -115,14 +122,14 @@ describe('Notebook Storage:', () => {
};
NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject);
NotebookStorage.setDefaultNotebookSection(section);
NotebookStorage.setDefaultNotebookSectionId(section.id);
const defaultNotebook = NotebookStorage.getDefaultNotebook();
const newSection = defaultNotebook.section;
expect(JSON.stringify(section)).toBe(JSON.stringify(newSection));
const defaultSectionId = defaultNotebook.defaultSectionId;
expect(section.id).toBe(defaultSectionId);
});
it('has correct page on setDefaultNotebookPage', () => {
it('has correct page on setDefaultNotebookPageId', () => {
const page = {
id: 'new-temp-page',
isDefault: true,
@@ -132,10 +139,52 @@ describe('Notebook Storage:', () => {
};
NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject);
NotebookStorage.setDefaultNotebookPage(page);
NotebookStorage.setDefaultNotebookPageId(page.id);
const defaultNotebook = NotebookStorage.getDefaultNotebook();
const newPage = defaultNotebook.page;
expect(JSON.stringify(page)).toBe(JSON.stringify(newPage));
const newPageId = defaultNotebook.defaultPageId;
expect(page.id).toBe(newPageId);
});
describe('is getNotebookSectionAndPage function searches and returns correct,', () => {
let section;
let page;
beforeEach(() => {
const sectionId = 'temp-section';
const pageId = 'temp-page';
const sectionAndpage = NotebookStorage.getNotebookSectionAndPage(domainObject, sectionId, pageId);
section = sectionAndpage.section;
page = sectionAndpage.page;
});
it('id for section from notebook domain object', () => {
expect(section.id).toEqual('temp-section');
});
it('name for section from notebook domain object', () => {
expect(section.name).toEqual('section');
});
it('sectionTitle for section from notebook domain object', () => {
expect(section.sectionTitle).toEqual('Section');
});
it('number of pages for section from notebook domain object', () => {
expect(section.pages.length).toEqual(1);
});
it('id for page from notebook domain object', () => {
expect(page.id).toEqual('temp-page');
});
it('name for page from notebook domain object', () => {
expect(page.name).toEqual('page');
});
it('pageTitle for page from notebook domain object', () => {
expect(page.pageTitle).toEqual('Page');
});
});
});

View File

@@ -47,6 +47,7 @@ export default class CouchObjectProvider {
let provider = this;
let sharedWorker;
// eslint-disable-next-line no-undef
const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`;
sharedWorker = new SharedWorker(sharedWorkerURL);
@@ -378,7 +379,7 @@ export default class CouchObjectProvider {
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) {
if (Object.keys(this.observers).length === 0 && this.isObservingObjectChanges()) {
this.stopObservingObjectChanges();
}
}
@@ -436,7 +437,7 @@ export default class CouchObjectProvider {
if (!this.changesFeedSharedWorker) {
this.changesFeedSharedWorker = this.startSharedWorker();
if (typeof this.stopObservingObjectChanges === 'function') {
if (this.isObservingObjectChanges()) {
this.stopObservingObjectChanges();
}
@@ -458,7 +459,7 @@ export default class CouchObjectProvider {
let error = false;
if (typeof this.stopObservingObjectChanges === 'function') {
if (this.isObservingObjectChanges()) {
this.stopObservingObjectChanges();
}

View File

@@ -25,16 +25,16 @@
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
>
<plot-legend :cursor-locked="!!lockHighlightPoint"
:series="config.series.models"
:series="seriesModels"
:highlights="highlights"
:legend="config.legend"
:legend="legend"
@legendHoverChanged="legendHoverChanged"
/>
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
<y-axis v-if="config.series.models.length > 0"
<y-axis v-if="seriesModels.length > 0"
:tick-width="tickWidth"
:single-series="config.series.models.length === 1"
:series-model="config.series.models[0]"
:single-series="seriesModels.length === 1"
:series-model="seriesModels[0]"
@yKeyChanged="setYAxisKey"
@tickWidthChanged="onTickWidthChange"
/>
@@ -141,8 +141,8 @@
>
</div>
</div>
<x-axis v-if="config.series.models.length > 0 && !options.compact"
:series-model="config.series.models[0]"
<x-axis v-if="seriesModels.length > 0 && !options.compact"
:series-model="seriesModels[0]"
/>
</div>
@@ -213,7 +213,8 @@ export default {
plotHistory: [],
selectedXKeyOption: {},
xKeyOptions: [],
config: {},
seriesModels: [],
legend: {},
pending: 0,
isRealTime: this.openmct.time.clock() !== undefined,
loaded: false,
@@ -239,18 +240,13 @@ export default {
watch: {
plotTickWidth(newTickWidth) {
this.onTickWidthChange(newTickWidth, true);
},
gridLines(newGridLines) {
this.setGridLinesVisibility(newGridLines);
},
cursorGuide(newCursorGuide) {
this.setCursorGuideVisibility(newCursorGuide);
}
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.legend = this.config.legend;
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
@@ -290,14 +286,18 @@ export default {
config = new PlotConfigurationModel({
id: configId,
domainObject: this.domainObject,
openmct: this.openmct
openmct: this.openmct,
callback: (data) => {
this.data = data;
}
});
configStore.add(configId, config);
}
return config;
},
addSeries(series) {
addSeries(series, index) {
this.$set(this.seriesModels, index, series);
this.listenTo(series, 'change:xKey', (xKey) => {
this.setDisplayRange(series, xKey);
}, this);
@@ -323,6 +323,8 @@ export default {
return;
}
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.startLoading();
const options = {
size: this.$parent.$refs.plotWrapper.offsetWidth,
@@ -377,11 +379,8 @@ export default {
},
stopLoading() {
//TODO: Is Vue.$nextTick ok to replace $scope.$evalAsync?
this.$nextTick().then(() => {
this.pending -= 1;
this.updateLoading();
});
this.pending -= 1;
this.updateLoading();
},
updateLoading() {
@@ -394,12 +393,31 @@ export default {
});
},
clearData() {
clearSeries() {
this.config.series.forEach(function (series) {
series.reset();
});
},
compositionPathContainsId(domainObjectToClear) {
return domainObjectToClear.composition.some((compositionIdentifier) => {
return this.openmct.objects.areIdsEqual(compositionIdentifier, this.domainObject.identifier);
});
},
clearData(domainObjectToClear) {
// If we don't have an object to clear (global), or the IDs are equal, just clear the data.
// If we have an object to clear, but the IDs don't match, we need to check the composition
// of the object we've been asked to clear to see if it contains the id we're looking for.
// This happens with stacked plots for example.
// If we find the ID, clear the plot.
if (!domainObjectToClear
|| this.openmct.objects.areIdsEqual(domainObjectToClear.identifier, this.domainObject.identifier)
|| this.compositionPathContainsId(domainObjectToClear)) {
this.clearSeries();
}
},
setDisplayRange(series, xKey) {
if (this.config.series.length !== 1) {
return;
@@ -507,7 +525,7 @@ export default {
},
initialize() {
_.debounce(this.handleWindowResize, 400);
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);
this.plotContainerResizeObserver.observe(this.$parent.$refs.plotWrapper);
@@ -623,7 +641,7 @@ export default {
this.config.series.models.forEach(series => delete series.closest);
} else {
this.highlights = this.config.series.models
.filter(series => series.data.length > 0)
.filter(series => series.getSeriesData().length > 0)
.map(series => {
series.closest = series.nearestPoint(point);
@@ -927,16 +945,8 @@ export default {
this.userViewportChangeEnd();
},
setCursorGuideVisibility(cursorGuide) {
this.cursorGuide = cursorGuide === true;
},
setGridLinesVisibility(gridLines) {
this.gridLines = gridLines === true;
},
setYAxisKey(yKey) {
this.config.series.models[0].emit('change:yKey', yKey);
this.config.series.models[0].set('yKey', yKey);
},
pause() {

View File

@@ -106,8 +106,9 @@ export default {
},
toggleXKeyOption() {
const selectedXKey = this.selectedXKeyOptionKey;
const dataForSelectedXKey = this.seriesModel.data
? this.seriesModel.data[0][selectedXKey]
const seriesData = this.seriesModel.getSeriesData();
const dataForSelectedXKey = seriesData
? seriesData[0][selectedXKey]
: undefined;
if (dataForSelectedXKey !== undefined) {

View File

@@ -36,7 +36,7 @@ export default class MCTChartAlarmPointSet {
this.listenTo(series, 'reset', this.reset, this);
this.listenTo(series, 'destroy', this.destroy, this);
series.data.forEach(function (point, index) {
this.series.getSeriesData().forEach(function (point, index) {
this.append(point, index, series);
}, this);
}

View File

@@ -36,7 +36,7 @@ export default class MCTChartSeriesElement {
this.listenTo(series, 'remove', this.remove, this);
this.listenTo(series, 'reset', this.reset, this);
this.listenTo(series, 'destroy', this.destroy, this);
series.data.forEach(function (point, index) {
this.series.getSeriesData().forEach(function (point, index) {
this.append(point, index, series);
}, this);
}
@@ -133,7 +133,7 @@ export default class MCTChartSeriesElement {
this.buffer = new Float32Array(20000);
this.count = 0;
if (this.offset.x) {
this.series.data.forEach(function (point, index) {
this.series.getSeriesData().forEach(function (point, index) {
this.append(point, index, this.series);
}, this);
}

View File

@@ -107,6 +107,7 @@ export default class PlotConfigurationModel extends Model {
updateDomainObject(domainObject) {
this.set('domainObject', domainObject);
}
/**
* Clean up all objects and remove all listeners.
*/

View File

@@ -22,6 +22,7 @@
import _ from 'lodash';
import Model from "./Model";
import { MARKER_SHAPES } from '../draw/MarkerShapes';
import configStore from "../configuration/configStore";
/**
* Plot series handle interpreting telemetry metadata for a single telemetry
@@ -62,7 +63,6 @@ import { MARKER_SHAPES } from '../draw/MarkerShapes';
export default class PlotSeries extends Model {
constructor(options) {
super(options);
this.data = [];
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
this.listenTo(this, 'change:yKey', this.onYKeyChange, this);
@@ -115,6 +115,8 @@ export default class PlotSeries extends Model {
this.openmct = options.openmct;
this.domainObject = options.domainObject;
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.dataStoreId = `data-${options.collection.plot.id}-${this.keyString}`;
this.updateSeriesData([]);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject);
this.limits = [];
@@ -182,7 +184,8 @@ export default class PlotSeries extends Model {
.telemetry
.request(this.domainObject, options)
.then(function (points) {
const newPoints = _(this.data)
const data = this.getSeriesData();
const newPoints = _(data)
.concat(points)
.sortBy(this.getXVal)
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
@@ -236,7 +239,7 @@ export default class PlotSeries extends Model {
*/
resetStats() {
this.unset('stats');
this.data.forEach(this.updateStats, this);
this.getSeriesData().forEach(this.updateStats, this);
}
/**
@@ -244,7 +247,7 @@ export default class PlotSeries extends Model {
* data to series after reset.
*/
reset(newData) {
this.data = [];
this.updateSeriesData([]);
this.resetStats();
this.emit('reset');
if (newData) {
@@ -258,8 +261,9 @@ export default class PlotSeries extends Model {
*/
nearestPoint(xValue) {
const insertIndex = this.sortedIndex(xValue);
const lowPoint = this.data[insertIndex - 1];
const highPoint = this.data[insertIndex];
const data = this.getSeriesData();
const lowPoint = data[insertIndex - 1];
const highPoint = data[insertIndex];
const indexVal = this.getXVal(xValue);
const lowDistance = lowPoint
? indexVal - this.getXVal(lowPoint)
@@ -292,7 +296,7 @@ export default class PlotSeries extends Model {
* @private
*/
sortedIndex(point) {
return _.sortedIndexBy(this.data, point, this.getXVal);
return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal);
}
/**
* Update min/max stats for the series.
@@ -346,9 +350,10 @@ export default class PlotSeries extends Model {
* a point to the end without dupe checking.
*/
add(point, appendOnly) {
let insertIndex = this.data.length;
let data = this.getSeriesData();
let insertIndex = data.length;
const currentYVal = this.getYVal(point);
const lastYVal = this.getYVal(this.data[insertIndex - 1]);
const lastYVal = this.getYVal(data[insertIndex - 1]);
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
console.warn('[Plot] Invalid Y Values detected');
@@ -358,18 +363,19 @@ export default class PlotSeries extends Model {
if (!appendOnly) {
insertIndex = this.sortedIndex(point);
if (this.getXVal(this.data[insertIndex]) === this.getXVal(point)) {
if (this.getXVal(data[insertIndex]) === this.getXVal(point)) {
return;
}
if (this.getXVal(this.data[insertIndex - 1]) === this.getXVal(point)) {
if (this.getXVal(data[insertIndex - 1]) === this.getXVal(point)) {
return;
}
}
this.updateStats(point);
point.mctLimitState = this.evaluate(point);
this.data.splice(insertIndex, 0, point);
data.splice(insertIndex, 0, point);
this.updateSeriesData(data);
this.emit('add', point, insertIndex, this);
}
@@ -386,8 +392,10 @@ export default class PlotSeries extends Model {
* @private
*/
remove(point) {
const index = this.data.indexOf(point);
this.data.splice(index, 1);
let data = this.getSeriesData();
const index = data.indexOf(point);
data.splice(index, 1);
this.updateSeriesData(data);
this.emit('remove', point, index, this);
}
/**
@@ -403,14 +411,16 @@ export default class PlotSeries extends Model {
purgeRecordsOutsideRange(range) {
const startIndex = this.sortedIndex(range.min);
const endIndex = this.sortedIndex(range.max) + 1;
const pointsToRemove = startIndex + (this.data.length - endIndex + 1);
let data = this.getSeriesData();
const pointsToRemove = startIndex + (data.length - endIndex + 1);
if (pointsToRemove > 0) {
if (pointsToRemove < 1000) {
this.data.slice(0, startIndex).forEach(this.remove, this);
this.data.slice(endIndex, this.data.length).forEach(this.remove, this);
data.slice(0, startIndex).forEach(this.remove, this);
data.slice(endIndex, data.length).forEach(this.remove, this);
this.updateSeriesData(data);
this.resetStats();
} else {
const newData = this.data.slice(startIndex, endIndex);
const newData = this.getSeriesData().slice(startIndex, endIndex);
this.reset(newData);
}
}
@@ -441,12 +451,13 @@ export default class PlotSeries extends Model {
}
}
getDisplayRange(xKey) {
const unsortedData = this.data;
this.data = [];
const unsortedData = this.getSeriesData();
this.updateSeriesData([]);
unsortedData.forEach(point => this.add(point, false));
const minValue = this.getXVal(this.data[0]);
const maxValue = this.getXVal(this.data[this.data.length - 1]);
let data = this.getSeriesData();
const minValue = this.getXVal(data[0]);
const maxValue = this.getXVal(data[data.length - 1]);
return {
min: minValue,
@@ -470,4 +481,18 @@ export default class PlotSeries extends Model {
return this.get('name') + (unit ? ' ' + unit : '');
}
/**
* Update the series data with the given value.
*/
updateSeriesData(data) {
configStore.add(this.dataStoreId, data);
}
/**
* Update the series data with the given value.
*/
getSeriesData() {
return configStore.get(this.dataStoreId) || [];
}
}

View File

@@ -25,7 +25,10 @@ function ConfigStore() {
ConfigStore.prototype.deleteStore = function (id) {
if (this.store[id]) {
this.store[id].destroy();
if (this.store[id].destroy) {
this.store[id].destroy();
}
delete this.store[id];
}
};

View File

@@ -176,7 +176,9 @@ DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) {
this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
this.gl.uniform4fv(this.uColor, color);
this.gl.uniform1i(this.uMarkerShape, shapeCode);
this.gl.drawArrays(drawType, 0, points);
if (points !== 0) {
this.gl.drawArrays(drawType, 0, points);
}
};
DrawWebGL.prototype.clear = function () {

View File

@@ -715,14 +715,15 @@ describe("the plugin", function () {
});
it("Adds a new point to the plot", (done) => {
let originalLength = config.series.models[0].data.length;
let originalLength = config.series.models[0].getSeriesData().length;
config.series.models[0].add({
utc: 2,
'some-key': 1,
'some-other-key': 2
});
Vue.nextTick(() => {
expect(config.series.models[0].data.length).toEqual(originalLength + 1);
const seriesData = config.series.models[0].getSeriesData();
expect(seriesData.length).toEqual(originalLength + 1);
done();
});
});

View File

@@ -68,7 +68,8 @@ define([
'./performanceIndicator/plugin',
'./CouchDBSearchFolder/plugin',
'./timeline/plugin',
'./hyperlink/plugin'
'./hyperlink/plugin',
'./DeviceClassifier/plugin'
], function (
_,
UTCTimeSystem,
@@ -117,7 +118,8 @@ define([
PerformanceIndicator,
CouchDBSearchFolder,
Timeline,
Hyperlink
Hyperlink,
DeviceClassifier
) {
const bundleMap = {
LocalStorage: 'platform/persistence/local',
@@ -221,6 +223,7 @@ define([
plugins.CouchDBSearchFolder = CouchDBSearchFolder.default;
plugins.Timeline = Timeline.default;
plugins.Hyperlink = Hyperlink.default;
plugins.DeviceClassifier = DeviceClassifier.default;
return plugins;
});

View File

@@ -137,7 +137,10 @@ describe("the RemoteClock plugin", () => {
it('will request the latest datum for the object it received and process the datum returned', () => {
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
expect(boundsCallback).toHaveBeenCalledWith({ start: TIME_VALUE + OFFSET_START, end: TIME_VALUE + OFFSET_END }, true);
expect(boundsCallback).toHaveBeenCalledWith({
start: TIME_VALUE + OFFSET_START,
end: TIME_VALUE + OFFSET_END
}, true);
});
it('will set up subscriptions correctly', () => {

View File

@@ -22,7 +22,7 @@
import RemoteClock from "./RemoteClock";
/**
* Install a clock that uses a configurable telemetry endpoint.
* Install a clock that uses a configurable telemetry endpoint.
*/
export default function (identifier) {

View File

@@ -36,7 +36,7 @@ define([
}
/**
* Convets all objects in an object make from old format objects to new
* Converts all objects in an object make from old format objects to new
* format objects.
*/
function convertToNewObjects(oldObjectMap) {

View File

@@ -115,7 +115,7 @@ define([
expect(condition.evaluate(telemetryState)).toBe(true);
});
it('can evalute "if any object matches"', function () {
it('can evaluate "if any object matches"', function () {
condition = new SummaryWidgetCondition({
object: 'any',
key: 'raw',

View File

@@ -379,7 +379,7 @@ define(['../src/ConditionManager'], function (ConditionManager) {
telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]);
});
it('updates its LAD cache upon recieving telemetry and invokes the appropriate handlers', function () {
it('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () {
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1');
expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a different string');
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2');

View File

@@ -48,7 +48,7 @@
display: flex;
flex-flow: column nowrap;
flex: 1 1 auto;
height: 0; // Chrome 73 oveflow bug fix
height: 0; // Chrome 73 overflow bug fix
}
&__empty-message {

View File

@@ -23,20 +23,18 @@
define([
'EventEmitter',
'lodash',
'./collections/BoundedTableRowCollection',
'./collections/FilteredTableRowCollection',
'./TelemetryTableNameColumn',
'./collections/TableRowCollection',
'./TelemetryTableRow',
'./TelemetryTableNameColumn',
'./TelemetryTableColumn',
'./TelemetryTableUnitColumn',
'./TelemetryTableConfiguration'
], function (
EventEmitter,
_,
BoundedTableRowCollection,
FilteredTableRowCollection,
TelemetryTableNameColumn,
TableRowCollection,
TelemetryTableRow,
TelemetryTableNameColumn,
TelemetryTableColumn,
TelemetryTableUnitColumn,
TelemetryTableConfiguration
@@ -48,20 +46,23 @@ define([
this.domainObject = domainObject;
this.openmct = openmct;
this.rowCount = 100;
this.subscriptions = {};
this.tableComposition = undefined;
this.telemetryObjects = [];
this.datumCache = [];
this.outstandingRequests = 0;
this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
this.paused = false;
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.telemetryObjects = {};
this.telemetryCollections = {};
this.delayedActions = [];
this.outstandingRequests = 0;
this.addTelemetryObject = this.addTelemetryObject.bind(this);
this.removeTelemetryObject = this.removeTelemetryObject.bind(this);
this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this);
this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this);
this.isTelemetryObject = this.isTelemetryObject.bind(this);
this.refreshData = this.refreshData.bind(this);
this.requestDataFor = this.requestDataFor.bind(this);
this.updateFilters = this.updateFilters.bind(this);
this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this);
@@ -102,8 +103,7 @@ define([
}
createTableRowCollections() {
this.boundedRows = new BoundedTableRowCollection(this.openmct);
this.filteredRows = new FilteredTableRowCollection(this.boundedRows);
this.tableRows = new TableRowCollection();
//Fetch any persisted default sort
let sortOptions = this.configuration.getConfiguration().sortOptions;
@@ -113,11 +113,14 @@ define([
key: this.openmct.time.timeSystem().key,
direction: 'asc'
};
this.filteredRows.sortBy(sortOptions);
this.tableRows.sortBy(sortOptions);
this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);
}
loadComposition() {
this.tableComposition = this.openmct.composition.get(this.domainObject);
if (this.tableComposition !== undefined) {
this.tableComposition.load().then((composition) => {
@@ -132,66 +135,64 @@ define([
addTelemetryObject(telemetryObject) {
this.addColumnsForObject(telemetryObject, true);
this.requestDataFor(telemetryObject);
this.subscribeTo(telemetryObject);
this.telemetryObjects.push(telemetryObject);
const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let requestOptions = this.buildOptionsFromConfiguration(telemetryObject);
let columnMap = this.getColumnMapForObject(keyString);
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
this.incrementOutstandingRequests();
const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator);
const telemetryRemover = this.getTelemetryRemover();
this.removeTelemetryCollection(keyString);
this.telemetryCollections[keyString] = this.openmct.telemetry
.requestCollection(telemetryObject, requestOptions);
this.telemetryCollections[keyString].on('remove', telemetryRemover);
this.telemetryCollections[keyString].on('add', telemetryProcessor);
this.telemetryCollections[keyString].load();
this.decrementOutstandingRequests();
this.telemetryObjects[keyString] = {
telemetryObject,
keyString,
requestOptions,
columnMap,
limitEvaluator
};
this.emit('object-added', telemetryObject);
}
updateFilters(updatedFilters) {
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
getTelemetryProcessor(keyString, columnMap, limitEvaluator) {
return (telemetry) => {
//Check that telemetry object has not been removed since telemetry was requested.
if (!this.telemetryObjects[keyString]) {
return;
}
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
this.filters = deepCopiedFilters;
this.clearAndResubscribe();
} else {
this.filters = deepCopiedFilters;
}
let telemetryRows = telemetry.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
if (this.paused) {
this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));
} else {
this.tableRows.addRows(telemetryRows, 'add');
}
};
}
clearAndResubscribe() {
this.filteredRows.clear();
this.boundedRows.clear();
Object.keys(this.subscriptions).forEach(this.unsubscribe, this);
this.telemetryObjects.forEach(this.requestDataFor.bind(this));
this.telemetryObjects.forEach(this.subscribeTo.bind(this));
}
removeTelemetryObject(objectIdentifier) {
this.configuration.removeColumnsForObject(objectIdentifier, true);
let keyString = this.openmct.objects.makeKeyString(objectIdentifier);
this.boundedRows.removeAllRowsForObject(keyString);
this.unsubscribe(keyString);
this.telemetryObjects = this.telemetryObjects.filter((object) => !_.eq(objectIdentifier, object.identifier));
this.emit('object-removed', objectIdentifier);
}
requestDataFor(telemetryObject) {
this.incrementOutstandingRequests();
let requestOptions = this.buildOptionsFromConfiguration(telemetryObject);
return this.openmct.telemetry.request(telemetryObject, requestOptions)
.then(telemetryData => {
//Check that telemetry object has not been removed since telemetry was requested.
if (!this.telemetryObjects.includes(telemetryObject)) {
return;
}
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let columnMap = this.getColumnMapForObject(keyString);
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
this.processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator);
}).finally(() => {
this.decrementOutstandingRequests();
});
}
processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) {
let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
this.boundedRows.add(telemetryRows);
getTelemetryRemover() {
return (telemetry) => {
if (this.paused) {
this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry));
} else {
this.tableRows.removeRowsByData(telemetry);
}
};
}
/**
@@ -216,35 +217,72 @@ define([
}
}
// will pull all necessary information for all existing bounded telemetry
// and pass to table row collection to reset without making any new requests
// triggered by filtering
resetRowsFromAllData() {
let allRows = [];
Object.keys(this.telemetryCollections).forEach(keyString => {
let { columnMap, limitEvaluator } = this.telemetryObjects[keyString];
this.telemetryCollections[keyString].getAll().forEach(datum => {
allRows.push(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
});
});
this.tableRows.addRows(allRows, 'filter');
}
updateFilters(updatedFilters) {
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
this.filters = deepCopiedFilters;
this.tableRows.clear();
this.clearAndResubscribe();
} else {
this.filters = deepCopiedFilters;
}
}
clearAndResubscribe() {
let objectKeys = Object.keys(this.telemetryObjects);
this.tableRows.clear();
objectKeys.forEach((keyString) => {
this.addTelemetryObject(this.telemetryObjects[keyString].telemetryObject);
});
}
removeTelemetryObject(objectIdentifier) {
const keyString = this.openmct.objects.makeKeyString(objectIdentifier);
this.configuration.removeColumnsForObject(objectIdentifier, true);
this.tableRows.removeRowsByObject(keyString);
this.removeTelemetryCollection(keyString);
delete this.telemetryObjects[keyString];
this.emit('object-removed', objectIdentifier);
}
refreshData(bounds, isTick) {
if (!isTick && this.outstandingRequests === 0) {
this.filteredRows.clear();
this.boundedRows.clear();
this.boundedRows.sortByTimeSystem(this.openmct.time.timeSystem());
this.telemetryObjects.forEach(this.requestDataFor);
if (!isTick && this.tableRows.outstandingRequests === 0) {
this.tableRows.clear();
this.tableRows.sortBy({
key: this.openmct.time.timeSystem().key,
direction: 'asc'
});
this.tableRows.resubscribe();
}
}
clearData() {
this.filteredRows.clear();
this.boundedRows.clear();
this.tableRows.clear();
this.emit('refresh');
}
getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns();
if (columns[objectKeyString]) {
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
return map;
}, {});
}
return {};
}
addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
@@ -264,54 +302,18 @@ define([
});
}
createColumn(metadatum) {
return new TelemetryTableColumn(this.openmct, metadatum);
}
getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns();
createUnitColumn(metadatum) {
return new TelemetryTableUnitColumn(this.openmct, metadatum);
}
if (columns[objectKeyString]) {
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
subscribeTo(telemetryObject) {
let subscribeOptions = this.buildOptionsFromConfiguration(telemetryObject);
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let columnMap = this.getColumnMapForObject(keyString);
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
return map;
}, {});
}
this.subscriptions[keyString] = this.openmct.telemetry.subscribe(telemetryObject, (datum) => {
//Check that telemetry object has not been removed since telemetry was requested.
if (!this.telemetryObjects.includes(telemetryObject)) {
return;
}
if (this.paused) {
let realtimeDatum = {
datum,
columnMap,
keyString,
limitEvaluator
};
this.datumCache.push(realtimeDatum);
} else {
this.processRealtimeDatum(datum, columnMap, keyString, limitEvaluator);
}
}, subscribeOptions);
}
processDatumCache() {
this.datumCache.forEach(cachedDatum => {
this.processRealtimeDatum(cachedDatum.datum, cachedDatum.columnMap, cachedDatum.keyString, cachedDatum.limitEvaluator);
});
this.datumCache = [];
}
processRealtimeDatum(datum, columnMap, keyString, limitEvaluator) {
this.boundedRows.add(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
}
isTelemetryObject(domainObject) {
return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry');
return {};
}
buildOptionsFromConfiguration(telemetryObject) {
@@ -323,13 +325,20 @@ define([
return {filters} || {};
}
unsubscribe(keyString) {
this.subscriptions[keyString]();
delete this.subscriptions[keyString];
createColumn(metadatum) {
return new TelemetryTableColumn(this.openmct, metadatum);
}
createUnitColumn(metadatum) {
return new TelemetryTableUnitColumn(this.openmct, metadatum);
}
isTelemetryObject(domainObject) {
return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry');
}
sortBy(sortOptions) {
this.filteredRows.sortBy(sortOptions);
this.tableRows.sortBy(sortOptions);
if (this.openmct.editor.isEditing()) {
let configuration = this.configuration.getConfiguration();
@@ -338,21 +347,36 @@ define([
}
}
runDelayedActions() {
this.delayedActions.forEach(action => action());
this.delayedActions = [];
}
removeTelemetryCollection(keyString) {
if (this.telemetryCollections[keyString]) {
this.telemetryCollections[keyString].destroy();
this.telemetryCollections[keyString] = undefined;
delete this.telemetryCollections[keyString];
}
}
pause() {
this.paused = true;
this.boundedRows.unsubscribeFromBounds();
}
unpause() {
this.paused = false;
this.processDatumCache();
this.boundedRows.subscribeToBounds();
this.runDelayedActions();
}
destroy() {
this.boundedRows.destroy();
this.filteredRows.destroy();
Object.keys(this.subscriptions).forEach(this.unsubscribe, this);
this.tableRows.destroy();
this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData);
let keystrings = Object.keys(this.telemetryCollections);
keystrings.forEach(this.removeTelemetryCollection);
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('timeSystem', this.refreshData);

View File

@@ -1,166 +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(
[
'lodash',
'./SortedTableRowCollection'
],
function (
_,
SortedTableRowCollection
) {
class BoundedTableRowCollection extends SortedTableRowCollection {
constructor(openmct) {
super();
this.futureBuffer = new SortedTableRowCollection();
this.openmct = openmct;
this.sortByTimeSystem = this.sortByTimeSystem.bind(this);
this.bounds = this.bounds.bind(this);
this.sortByTimeSystem(openmct.time.timeSystem());
this.lastBounds = openmct.time.bounds();
this.subscribeToBounds();
}
addOne(item) {
let parsedValue = this.getValueForSortColumn(item);
// Insert into either in-bounds array, or the future buffer.
// Data in the future buffer will be re-evaluated for possible
// insertion on next bounds change
let beforeStartOfBounds = parsedValue < this.lastBounds.start;
let afterEndOfBounds = parsedValue > this.lastBounds.end;
if (!afterEndOfBounds && !beforeStartOfBounds) {
return super.addOne(item);
} else if (afterEndOfBounds) {
this.futureBuffer.addOne(item);
}
return false;
}
sortByTimeSystem(timeSystem) {
this.sortBy({
key: timeSystem.key,
direction: 'asc'
});
let formatter = this.openmct.telemetry.getValueFormatter({
key: timeSystem.key,
source: timeSystem.key,
format: timeSystem.timeFormat
});
this.parseTime = formatter.parse.bind(formatter);
this.futureBuffer.sortBy({
key: timeSystem.key,
direction: 'asc'
});
}
/**
* This function is optimized for ticking - it assumes that start and end
* bounds will only increase and as such this cannot be used for decreasing
* bounds changes.
*
* An implication of this is that data will not be discarded that exceeds
* the given end bounds. For arbitrary bounds changes, it's assumed that
* a telemetry requery is performed anyway, and the collection is cleared
* and repopulated.
*
* @fires TelemetryCollection#added
* @fires TelemetryCollection#discarded
* @param bounds
*/
bounds(bounds) {
let startChanged = this.lastBounds.start !== bounds.start;
let endChanged = this.lastBounds.end !== bounds.end;
let startIndex = 0;
let endIndex = 0;
let discarded = [];
let added = [];
let testValue = {
datum: {}
};
this.lastBounds = bounds;
if (startChanged) {
testValue.datum[this.sortOptions.key] = bounds.start;
// Calculate the new index of the first item within the bounds
startIndex = this.sortedIndex(this.rows, testValue);
discarded = this.rows.splice(0, startIndex);
}
if (endChanged) {
testValue.datum[this.sortOptions.key] = bounds.end;
// Calculate the new index of the last item in bounds
endIndex = this.sortedLastIndex(this.futureBuffer.rows, testValue);
added = this.futureBuffer.rows.splice(0, endIndex);
added.forEach((datum) => this.rows.push(datum));
}
if (discarded && discarded.length > 0) {
/**
* A `discarded` event is emitted when telemetry data fall out of
* bounds due to a bounds change event
* @type {object[]} discarded the telemetry data
* discarded as a result of the bounds change
*/
this.emit('remove', discarded);
}
if (added && added.length > 0) {
/**
* An `added` event is emitted when a bounds change results in
* received telemetry falling within the new bounds.
* @type {object[]} added the telemetry data that is now within bounds
*/
this.emit('add', added);
}
}
getValueForSortColumn(row) {
return this.parseTime(row.datum[this.sortOptions.key]);
}
unsubscribeFromBounds() {
this.openmct.time.off('bounds', this.bounds);
}
subscribeToBounds() {
this.openmct.time.on('bounds', this.bounds);
}
destroy() {
this.unsubscribeFromBounds();
}
}
return BoundedTableRowCollection;
});

View File

@@ -1,136 +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(
[
'./SortedTableRowCollection'
],
function (
SortedTableRowCollection
) {
class FilteredTableRowCollection extends SortedTableRowCollection {
constructor(masterCollection) {
super();
this.masterCollection = masterCollection;
this.columnFilters = {};
//Synchronize with master collection
this.masterCollection.on('add', this.add);
this.masterCollection.on('remove', this.remove);
//Default to master collection's sort options
this.sortOptions = masterCollection.sortBy();
}
setColumnFilter(columnKey, filter) {
filter = filter.trim().toLowerCase();
let rowsToFilter = this.getRowsToFilter(columnKey, filter);
if (filter.length === 0) {
delete this.columnFilters[columnKey];
} else {
this.columnFilters[columnKey] = filter;
}
this.rows = rowsToFilter.filter(this.matchesFilters, this);
this.emit('filter');
}
setColumnRegexFilter(columnKey, filter) {
filter = filter.trim();
let rowsToFilter = this.masterCollection.getRows();
this.columnFilters[columnKey] = new RegExp(filter);
this.rows = rowsToFilter.filter(this.matchesFilters, this);
this.emit('filter');
}
/**
* @private
*/
getRowsToFilter(columnKey, filter) {
if (this.isSubsetOfCurrentFilter(columnKey, filter)) {
return this.getRows();
} else {
return this.masterCollection.getRows();
}
}
/**
* @private
*/
isSubsetOfCurrentFilter(columnKey, filter) {
if (this.columnFilters[columnKey] instanceof RegExp) {
return false;
}
return this.columnFilters[columnKey]
&& filter.startsWith(this.columnFilters[columnKey])
// startsWith check will otherwise fail when filter cleared
// because anyString.startsWith('') === true
&& filter !== '';
}
addOne(row) {
return this.matchesFilters(row) && super.addOne(row);
}
/**
* @private
*/
matchesFilters(row) {
let doesMatchFilters = true;
Object.keys(this.columnFilters).forEach((key) => {
if (!doesMatchFilters || !this.rowHasColumn(row, key)) {
return false;
}
let formattedValue = row.getFormattedValue(key);
if (formattedValue === undefined) {
return false;
}
if (this.columnFilters[key] instanceof RegExp) {
doesMatchFilters = this.columnFilters[key].test(formattedValue);
} else {
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
}
});
return doesMatchFilters;
}
rowHasColumn(row, key) {
return Object.prototype.hasOwnProperty.call(row.columns, key);
}
destroy() {
this.masterCollection.off('add', this.add);
this.masterCollection.off('remove', this.remove);
}
}
return FilteredTableRowCollection;
});

View File

@@ -36,85 +36,72 @@ define(
/**
* @constructor
*/
class SortedTableRowCollection extends EventEmitter {
class TableRowCollection extends EventEmitter {
constructor() {
super();
this.dupeCheck = false;
this.rows = [];
this.columnFilters = {};
this.addRows = this.addRows.bind(this);
this.removeRowsByObject = this.removeRowsByObject.bind(this);
this.removeRowsByData = this.removeRowsByData.bind(this);
this.add = this.add.bind(this);
this.remove = this.remove.bind(this);
this.clear = this.clear.bind(this);
}
/**
* Add a datum or array of data to this telemetry collection
* @fires TelemetryCollection#added
* @param {object | object[]} rows
*/
add(rows) {
if (Array.isArray(rows)) {
this.dupeCheck = false;
removeRowsByObject(keyString) {
let removed = [];
let rowsAdded = rows.filter(this.addOne, this);
if (rowsAdded.length > 0) {
this.emit('add', rowsAdded);
}
this.rows = this.rows.filter((row) => {
if (row.objectKeyString === keyString) {
removed.push(row);
this.dupeCheck = true;
} else {
let wasAdded = this.addOne(rows);
if (wasAdded) {
this.emit('add', rows);
return false;
} else {
return true;
}
}
});
this.emit('remove', removed);
}
/**
* @private
*/
addOne(row) {
addRows(rows, type = 'add') {
if (this.sortOptions === undefined) {
throw 'Please specify sort options';
}
let isDuplicate = false;
let isFilterTriggeredReset = type === 'filter';
let anyActiveFilters = Object.keys(this.columnFilters).length > 0;
let rowsToAdd = !anyActiveFilters ? rows : rows.filter(this.matchesFilters, this);
// Going to check for duplicates. Bound the search problem to
// items around the given time. Use sortedIndex because it
// employs a binary search which is O(log n). Can use binary search
// because the array is guaranteed ordered due to sorted insertion.
let startIx = this.sortedIndex(this.rows, row);
let endIx = undefined;
if (this.dupeCheck && startIx !== this.rows.length) {
endIx = this.sortedLastIndex(this.rows, row);
// Create an array of potential dupes, based on having the
// same time stamp
let potentialDupes = this.rows.slice(startIx, endIx + 1);
// Search potential dupes for exact dupe
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, row));
// if type is filter, then it's a reset of all rows,
// need to wipe current rows
if (isFilterTriggeredReset) {
this.rows = [];
}
if (!isDuplicate) {
this.rows.splice(endIx || startIx, 0, row);
return true;
for (let row of rowsToAdd) {
let index = this.sortedIndex(this.rows, row);
this.rows.splice(index, 0, row);
}
return false;
// we emit filter no matter what to trigger
// an update of visible rows
if (rowsToAdd.length > 0 || isFilterTriggeredReset) {
this.emit(type, rowsToAdd);
}
}
sortedLastIndex(rows, testRow) {
return this.sortedIndex(rows, testRow, _.sortedLastIndex);
}
/**
* Finds the correct insertion point for the given row.
* Leverages lodash's `sortedIndex` function which implements a binary search.
* @private
*/
sortedIndex(rows, testRow, lodashFunction) {
sortedIndex(rows, testRow, lodashFunction = _.sortedIndexBy) {
if (this.rows.length === 0) {
return 0;
}
@@ -123,8 +110,6 @@ define(
const firstValue = this.getValueForSortColumn(this.rows[0]);
const lastValue = this.getValueForSortColumn(this.rows[this.rows.length - 1]);
lodashFunction = lodashFunction || _.sortedIndexBy;
if (this.sortOptions.direction === 'asc') {
if (testRowValue > lastValue) {
return this.rows.length;
@@ -162,6 +147,22 @@ define(
}
}
removeRowsByData(data) {
let removed = [];
this.rows = this.rows.filter((row) => {
if (data.includes(row.fullDatum)) {
removed.push(row);
return false;
} else {
return true;
}
});
this.emit('remove', removed);
}
/**
* Sorts the telemetry collection based on the provided sort field
* specifier. Subsequent inserts are sorted to maintain specified sport
@@ -205,6 +206,7 @@ define(
if (arguments.length > 0) {
this.sortOptions = sortOptions;
this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction);
this.emit('sort');
}
@@ -212,44 +214,114 @@ define(
return Object.assign({}, this.sortOptions);
}
removeAllRowsForObject(objectKeyString) {
let removed = [];
this.rows = this.rows.filter(row => {
if (row.objectKeyString === objectKeyString) {
removed.push(row);
setColumnFilter(columnKey, filter) {
filter = filter.trim().toLowerCase();
let wasBlank = this.columnFilters[columnKey] === undefined;
let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter);
if (filter.length === 0) {
delete this.columnFilters[columnKey];
} else {
this.columnFilters[columnKey] = filter;
}
if (isSubset || wasBlank) {
this.rows = this.rows.filter(this.matchesFilters, this);
this.emit('filter');
} else {
this.emit('resetRowsFromAllData');
}
}
setColumnRegexFilter(columnKey, filter) {
filter = filter.trim();
this.columnFilters[columnKey] = new RegExp(filter);
this.emit('resetRowsFromAllData');
}
getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns();
if (columns[objectKeyString]) {
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
return map;
}, {});
}
return {};
}
// /**
// * @private
// */
isSubsetOfCurrentFilter(columnKey, filter) {
if (this.columnFilters[columnKey] instanceof RegExp) {
return false;
}
return this.columnFilters[columnKey]
&& filter.startsWith(this.columnFilters[columnKey])
// startsWith check will otherwise fail when filter cleared
// because anyString.startsWith('') === true
&& filter !== '';
}
/**
* @private
*/
matchesFilters(row) {
let doesMatchFilters = true;
Object.keys(this.columnFilters).forEach((key) => {
if (!doesMatchFilters || !this.rowHasColumn(row, key)) {
return false;
}
return true;
let formattedValue = row.getFormattedValue(key);
if (formattedValue === undefined) {
return false;
}
if (this.columnFilters[key] instanceof RegExp) {
doesMatchFilters = this.columnFilters[key].test(formattedValue);
} else {
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
}
});
this.emit('remove', removed);
return doesMatchFilters;
}
getValueForSortColumn(row) {
return row.getParsedValue(this.sortOptions.key);
}
remove(removedRows) {
this.rows = this.rows.filter(row => {
return removedRows.indexOf(row) === -1;
});
this.emit('remove', removedRows);
rowHasColumn(row, key) {
return Object.prototype.hasOwnProperty.call(row.columns, key);
}
getRows() {
return this.rows;
}
getRowsLength() {
return this.rows.length;
}
getValueForSortColumn(row) {
return row.getParsedValue(this.sortOptions.key);
}
clear() {
let removedRows = this.rows;
this.rows = [];
this.emit('remove', removedRows);
}
destroy() {
this.removeAllListeners();
}
}
return SortedTableRowCollection;
return TableRowCollection;
});

View File

@@ -466,22 +466,21 @@ export default {
this.table.on('object-added', this.addObject);
this.table.on('object-removed', this.removeObject);
this.table.on('outstanding-requests', this.outstandingRequests);
this.table.on('refresh', this.clearRowsAndRerender);
this.table.on('historical-rows-processed', this.checkForMarkedRows);
this.table.on('outstanding-requests', this.outstandingRequests);
this.table.filteredRows.on('add', this.rowsAdded);
this.table.filteredRows.on('remove', this.rowsRemoved);
this.table.filteredRows.on('sort', this.updateVisibleRows);
this.table.filteredRows.on('filter', this.updateVisibleRows);
this.table.tableRows.on('add', this.rowsAdded);
this.table.tableRows.on('remove', this.rowsRemoved);
this.table.tableRows.on('sort', this.updateVisibleRows);
this.table.tableRows.on('filter', this.updateVisibleRows);
//Default sort
this.sortOptions = this.table.filteredRows.sortBy();
this.sortOptions = this.table.tableRows.sortBy();
this.scrollable = this.$el.querySelector('.js-telemetry-table__body-w');
this.contentTable = this.$el.querySelector('.js-telemetry-table__content');
this.sizingTable = this.$el.querySelector('.js-telemetry-table__sizing');
this.headersHolderEl = this.$el.querySelector('.js-table__headers-w');
this.table.configuration.on('change', this.updateConfiguration);
this.calculateTableSize();
@@ -493,13 +492,14 @@ export default {
destroyed() {
this.table.off('object-added', this.addObject);
this.table.off('object-removed', this.removeObject);
this.table.off('outstanding-requests', this.outstandingRequests);
this.table.off('historical-rows-processed', this.checkForMarkedRows);
this.table.off('refresh', this.clearRowsAndRerender);
this.table.off('outstanding-requests', this.outstandingRequests);
this.table.filteredRows.off('add', this.rowsAdded);
this.table.filteredRows.off('remove', this.rowsRemoved);
this.table.filteredRows.off('sort', this.updateVisibleRows);
this.table.filteredRows.off('filter', this.updateVisibleRows);
this.table.tableRows.off('add', this.rowsAdded);
this.table.tableRows.off('remove', this.rowsRemoved);
this.table.tableRows.off('sort', this.updateVisibleRows);
this.table.tableRows.off('filter', this.updateVisibleRows);
this.table.configuration.off('change', this.updateConfiguration);
@@ -517,13 +517,13 @@ export default {
let start = 0;
let end = VISIBLE_ROW_COUNT;
let filteredRows = this.table.filteredRows.getRows();
let filteredRowsLength = filteredRows.length;
let tableRows = this.table.tableRows.getRows();
let tableRowsLength = tableRows.length;
this.totalNumberOfRows = filteredRowsLength;
this.totalNumberOfRows = tableRowsLength;
if (filteredRowsLength < VISIBLE_ROW_COUNT) {
end = filteredRowsLength;
if (tableRowsLength < VISIBLE_ROW_COUNT) {
end = tableRowsLength;
} else {
let firstVisible = this.calculateFirstVisibleRow();
let lastVisible = this.calculateLastVisibleRow();
@@ -535,15 +535,15 @@ export default {
if (start < 0) {
start = 0;
end = Math.min(VISIBLE_ROW_COUNT, filteredRowsLength);
} else if (end >= filteredRowsLength) {
end = filteredRowsLength;
end = Math.min(VISIBLE_ROW_COUNT, tableRowsLength);
} else if (end >= tableRowsLength) {
end = tableRowsLength;
start = end - VISIBLE_ROW_COUNT + 1;
}
}
this.rowOffset = start;
this.visibleRows = filteredRows.slice(start, end);
this.visibleRows = tableRows.slice(start, end);
this.updatingView = false;
});
@@ -630,19 +630,19 @@ export default {
filterChanged(columnKey) {
if (this.enableRegexSearch[columnKey]) {
if (this.isCompleteRegex(this.filters[columnKey])) {
this.table.filteredRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1, -1));
this.table.tableRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1, -1));
} else {
return;
}
} else {
this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
this.table.tableRows.setColumnFilter(columnKey, this.filters[columnKey]);
}
this.setHeight();
},
clearFilter(columnKey) {
this.filters[columnKey] = '';
this.table.filteredRows.setColumnFilter(columnKey, '');
this.table.tableRows.setColumnFilter(columnKey, '');
this.setHeight();
},
rowsAdded(rows) {
@@ -674,8 +674,8 @@ export default {
* Calculates height based on total number of rows, and sets table height.
*/
setHeight() {
let filteredRowsLength = this.table.filteredRows.getRows().length;
this.totalHeight = this.rowHeight * filteredRowsLength - 1;
let tableRowsLength = this.table.tableRows.getRowsLength();
this.totalHeight = this.rowHeight * tableRowsLength - 1;
// Set element height directly to avoid having to wait for Vue to update DOM
// which causes subsequent scroll to use an out of date height.
this.contentTable.style.height = this.totalHeight + 'px';
@@ -689,13 +689,13 @@ export default {
});
},
exportAllDataAsCSV() {
const justTheData = this.table.filteredRows.getRows()
const justTheData = this.table.tableRows.getRows()
.map(row => row.getFormattedDatum(this.headers));
this.exportAsCSV(justTheData);
},
exportMarkedDataAsCSV() {
const data = this.table.filteredRows.getRows()
const data = this.table.tableRows.getRows()
.filter(row => row.marked === true)
.map(row => row.getFormattedDatum(this.headers));
@@ -900,7 +900,7 @@ export default {
let lastRowToBeMarked = this.visibleRows[rowIndex];
let allRows = this.table.filteredRows.getRows();
let allRows = this.table.tableRows.getRows();
let firstRowIndex = allRows.indexOf(this.markedRows[0]);
let lastRowIndex = allRows.indexOf(lastRowToBeMarked);
@@ -923,17 +923,17 @@ export default {
},
checkForMarkedRows() {
this.isShowingMarkedRowsOnly = false;
this.markedRows = this.table.filteredRows.getRows().filter(row => row.marked);
this.markedRows = this.table.tableRows.getRows().filter(row => row.marked);
},
showRows(rows) {
this.table.filteredRows.rows = rows;
this.table.filteredRows.emit('filter');
this.table.tableRows.rows = rows;
this.table.emit('filter');
},
toggleMarkedRows(flag) {
if (flag) {
this.isShowingMarkedRowsOnly = true;
this.userScroll = this.scrollable.scrollTop;
this.allRows = this.table.filteredRows.getRows();
this.allRows = this.table.tableRows.getRows();
this.showRows(this.markedRows);
this.setHeight();

View File

@@ -48,6 +48,8 @@ describe("the plugin", () => {
let tablePlugin;
let element;
let child;
let historicalProvider;
let originalRouterPath;
let unlistenConfigMutation;
beforeEach((done) => {
@@ -58,7 +60,12 @@ describe("the plugin", () => {
tablePlugin = new TablePlugin();
openmct.install(tablePlugin);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
historicalProvider = {
request: () => {
return Promise.resolve([]);
}
};
spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
element = document.createElement('div');
child = document.createElement('div');
@@ -78,6 +85,8 @@ describe("the plugin", () => {
callBack();
});
originalRouterPath = openmct.router.path;
openmct.on('start', done);
openmct.startHeadless();
});
@@ -190,11 +199,12 @@ describe("the plugin", () => {
let telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => {
historicalProvider.request = () => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
});
};
openmct.router.path = [testTelemetryObject];
@@ -208,6 +218,10 @@ describe("the plugin", () => {
return telemetryPromise.then(() => Vue.nextTick());
});
afterEach(() => {
openmct.router.path = originalRouterPath;
});
it("Renders a row for every telemetry datum returned", () => {
let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(rows.length).toBe(3);
@@ -256,14 +270,14 @@ describe("the plugin", () => {
});
it("Supports filtering telemetry by regular text search", () => {
tableInstance.filteredRows.setColumnFilter("some-key", "1");
tableInstance.tableRows.setColumnFilter("some-key", "1");
return Vue.nextTick().then(() => {
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(1);
tableInstance.filteredRows.setColumnFilter("some-key", "");
tableInstance.tableRows.setColumnFilter("some-key", "");
return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
@@ -274,14 +288,14 @@ describe("the plugin", () => {
});
it("Supports filtering using Regex", () => {
tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value$");
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$");
return Vue.nextTick().then(() => {
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(0);
tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value");
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');

Some files were not shown because too many files have changed in this diff Show More