Compare commits

..

4 Commits

Author SHA1 Message Date
Henry Hsu
2def6dd634 add mmgis dependency 2021-07-19 11:32:24 -07:00
David Tsay
16fe1b55c6 bubble activity logic to parent 2021-07-13 17:06:11 -07:00
David Tsay
8037c372b0 add missing copyrights 2021-07-13 16:33:26 -07:00
David Tsay
216a349282 allow custom details while retaining current domain object details 2021-07-13 16:32:22 -07:00
172 changed files with 4098 additions and 6887 deletions

View File

@@ -1,69 +1,36 @@
version: 2.1
executors:
linux:
docker:
- image: cimg/base:stable
orbs:
node: circleci/node@4.5.1
browser-tools: circleci/browser-tools@1.1.3
version: 2
jobs:
test:
parameters:
node-version:
type: string
browser:
type: string
always-pass:
type: boolean
executor: linux
build:
docker:
- image: circleci/node:13-browsers
environment:
CHROME_BIN: "/usr/bin/google-chrome"
steps:
- checkout
- restore_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
- node/install:
node-version: << parameters.node-version >>
- node/install-packages:
override-ci-command: npm install
- when: # Just to save time until caching saves the browser bin
condition:
equal: [ "FirefoxESR", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox:
version: "78.11.0esr" #https://archive.mozilla.org/pub/firefox/releases/
- when: # Just to save time until caching saves the browser bin
condition:
equal: [ "ChromeHeadless", <<parameters.browser>> ]
steps:
- browser-tools/install-chrome:
replace-existing: false
- save_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
paths:
- ~/.npm
- ~/.cache
- node_modules
- run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
path: dist/reports/
- checkout
- run:
name: Update npm
command: 'sudo npm install -g npm@latest'
- restore_cache:
key: dependency-cache-{{ checksum "package.json" }}
- run:
name: Installing dependencies (npm install)
command: npm install
- save_cache:
key: dependency-cache-{{ checksum "package.json" }}
paths:
- node_modules
- run:
name: npm run test:coverage
command: npm run test:coverage
- run:
name: npm run lint
command: npm run lint
- store_artifacts:
path: dist
prefix: dist
workflows:
matrix-tests:
version: 2
test:
jobs:
- test:
name: node10-chrome
node-version: lts/dubnium
browser: ChromeHeadless
always-pass: false
- test:
name: node12-firefoxESR
node-version: lts/erbium
browser: FirefoxESR
always-pass: true
- test:
name: node14-chrome
node-version: lts/fermium
browser: ChromeHeadless
always-pass: true
- build

View File

@@ -1,12 +1,11 @@
<!--- This is for filing bugs. If you have a general question, please -->
<!--- visit https://github.com/nasa/openmct/discussions -->
---
name: Bug report
name: Bug Report
about: File a Bug !
title: ''
labels: type:bug
assignees: ''
---
<!--- Focus on user impact in the title. Use the Summary Field to -->
<!--- describe the problem technically. -->
@@ -36,7 +35,7 @@ assignees: ''
#### Environment
* Open MCT Version: <!--- date of build, version, or SHA -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yams? -->
* OS:
* Browser:

View File

@@ -1,5 +1 @@
blank_issues_enabled: true
contact_links:
- name: Discussions
url: https://github.com/nasa/openmct/discussions
about: Got a question?
blank_issues_enabled: false

View File

@@ -1,20 +0,0 @@
---
name: Enhancement request
about: Suggest an enhancement or new improvement for this project
title: ''
labels: type:enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -13,8 +13,6 @@ jobs:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.version }}
- uses: actions/setup-node@v2
with:
node-version: '14'
- uses: actions/setup-node@v1
- run: npm install && npm install -g @lhci/cli #Don't want to include this in our deps
- run: lhci autorun

View File

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

View File

@@ -1,4 +1,4 @@
import Vue from 'vue';
import Vue from 'Vue';
import HelloWorld from './HelloWorld.vue';
function SimpleVuePlugin() {

View File

@@ -88,7 +88,6 @@
openmct.install(openmct.plugins.ExampleImagery());
openmct.install(openmct.plugins.PlanLayout());
openmct.install(openmct.plugins.Timeline());
openmct.install(openmct.plugins.Hyperlink());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(openmct.plugins.AutoflowView({
type: "telemetry.panel"

View File

@@ -23,9 +23,9 @@
/*global module,process*/
const devMode = process.env.NODE_ENV !== 'production';
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'FirefoxHeadless'];
const coverageEnabled = process.env.COVERAGE === 'true';
const reporters = ['progress', 'html', 'junit'];
const reporters = ['progress', 'html'];
if (coverageEnabled) {
reporters.push('coverage-istanbul');
@@ -59,8 +59,7 @@ module.exports = (config) => {
browsers: browsers,
client: {
jasmine: {
random: false,
timeoutInterval: 30000
random: false
}
},
customLaunchers: {
@@ -68,10 +67,6 @@ module.exports = (config) => {
base: 'Chrome',
flags: ['--remote-debugging-port=9222'],
debug: true
},
FirefoxESR: {
base: 'FirefoxHeadless',
name: 'FirefoxESR'
}
},
colors: true,
@@ -83,21 +78,12 @@ module.exports = (config) => {
preserveDescribeNesting: true,
foldAll: false
},
junitReporter: {
outputDir: "dist/reports/tests",
outputFile: "test-results.xml",
useBrowserName: false
},
browserConsoleLogOptions: {
level: "error",
format: "%b %T: %m",
terminal: true
},
browserConsoleLogOptions: { level: "error", format: "%b %T: %m", terminal: true },
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
dir: process.env.CIRCLE_ARTIFACTS
? process.env.CIRCLE_ARTIFACTS + '/coverage'
: "dist/reports/coverage",
dir: process.env.CIRCLE_ARTIFACTS ?
process.env.CIRCLE_ARTIFACTS + '/coverage' :
"dist/reports/coverage",
reports: ['html', 'lcovonly', 'text-summary'],
thresholds: {
global: {

View File

@@ -1,8 +1,10 @@
{
"name": "openmct",
"version": "1.7.6-SNAPSHOT",
"version": "1.7.4",
"description": "The Open MCT core platform",
"dependencies": {},
"dependencies": {
"openmct-mmgis": "git+https://trunk.arc.nasa.gov/bitbucket/scm/vipergds/openmct-mmgis.git#api-mmgis-inspector"
},
"devDependencies": {
"angular": ">=1.8.0",
"angular-route": "1.4.14",
@@ -37,18 +39,17 @@
"html2canvas": "^1.0.0-rc.7",
"imports-loader": "^0.8.0",
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.7.1",
"jasmine-core": "^3.1.0",
"jsdoc": "^3.3.2",
"karma": "6.3.4",
"karma": "5.1.1",
"karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "2.1.1",
"karma-cli": "2.0.0",
"karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-junit-reporter": "2.0.1",
"karma-firefox-launcher": "1.3.0",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "4.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-jasmine": "3.3.1",
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "4.0.2",
"location-bar": "^3.0.1",
"lodash": "^4.17.12",
@@ -90,7 +91,6 @@
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
"test:coverage:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"verify": "concurrently 'npm:test' 'npm:lint'",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
@@ -102,9 +102,6 @@
"type": "git",
"url": "https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=10.10.2 <16.0.0"
},
"author": "",
"license": "Apache-2.0",
"private": true

View File

@@ -181,7 +181,7 @@ define([
],
"category": "contextual",
"name": "Stop",
"cssClass": "icon-box-round-corners",
"cssClass": "icon-box",
"priority": "preferred"
}
],

View File

@@ -101,7 +101,7 @@ define(
name: "Pause"
});
mockStop.getMetadata.and.returnValue({
cssClass: "icon-box-round-corners",
cssClass: "icon-box",
name: "Stop"
});
mockScope.domainObject = mockDomainObject;

View File

@@ -0,0 +1,120 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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/HyperlinkController',
'./res/templates/hyperlink.html'
], function (
HyperlinkController,
hyperlinkTemplate
) {
return {
name: "platform/features/hyperlink",
definition: {
"name": "Hyperlink",
"description": "Insert a hyperlink to reference a link",
"extensions": {
"types": [
{
"key": "hyperlink",
"name": "Hyperlink",
"cssClass": "icon-chain-links",
"description": "A hyperlink to redirect to a different link",
"features": ["creation"],
"properties": [
{
"key": "url",
"name": "URL",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayText",
"name": "Text to Display",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayFormat",
"name": "Display Format",
"control": "select",
"options": [
{
"name": "Link",
"value": "link"
},
{
"value": "button",
"name": "Button"
}
],
"cssClass": "l-inline"
},
{
"key": "openNewTab",
"name": "Tab to Open Hyperlink",
"control": "select",
"options": [
{
"name": "Open in this tab",
"value": "thisTab"
},
{
"value": "newTab",
"name": "Open in a new tab"
}
],
"cssClass": "l-inline"
}
],
"model": {
"displayFormat": "link",
"openNewTab": "thisTab",
"removeTitle": true
}
}
],
"views": [
{
"key": "hyperlink",
"type": "hyperlink",
"name": "Hyperlink Display",
"template": hyperlinkTemplate,
"editable": false
}
],
"controllers": [
{
"key": "HyperlinkController",
"implementation": HyperlinkController,
"depends": ["$scope"]
}
]
}
}
};
});

View File

@@ -0,0 +1,28 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<a class="c-hyperlink u-links" ng-controller="HyperlinkController as hyperlink" href="{{domainObject.getModel().url}}"
ng-attr-target="{{hyperlink.openNewTab() ? '_blank' : undefined}}"
ng-class="{
'c-hyperlink--button u-fills-container' : hyperlink.isButton(),
'c-hyperlink--link' : !hyperlink.isButton() }">
<span class="c-hyperlink__label">{{domainObject.getModel().displayText}}</span>
</a>

View File

@@ -0,0 +1,61 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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.
*****************************************************************************/
/**
* This bundle adds the Hyperlink object type, which can be used to add hyperlinks as a domain Object type
and into display Layouts as either a button or link that can be chosen to open in either the same tab or
create a new tab to open the link in
* @namespace platform/features/hyperlink
*/
define(
[],
function () {
function HyperlinkController($scope) {
this.$scope = $scope;
}
/**Function to analyze the location in which to open the hyperlink
@returns true if the hyperlink is chosen to open in a different tab, false if the same tab
**/
HyperlinkController.prototype.openNewTab = function () {
if (this.$scope.domainObject.getModel().openNewTab === "thisTab") {
return false;
} else {
return true;
}
};
/**Function to specify the format in which the hyperlink should be created
@returns true if the hyperlink is chosen to be created as a button, false if a link
**/
HyperlinkController.prototype.isButton = function () {
if (this.$scope.domainObject.getModel().displayFormat === "link") {
return false;
}
return true;
};
return HyperlinkController;
}
);

View File

@@ -0,0 +1,89 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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/HyperlinkController"],
function (HyperlinkController) {
describe("The controller for hyperlinks", function () {
var domainObject,
controller,
scope;
beforeEach(function () {
scope = jasmine.createSpyObj(
"$scope",
["domainObject"]
);
domainObject = jasmine.createSpyObj(
"domainObject",
["getModel"]
);
scope.domainObject = domainObject;
controller = new HyperlinkController(scope);
});
it("knows when it should open a new tab", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "link",
"openNewTab": "newTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.openNewTab())
.toBe(true);
});
it("knows when it is a button", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "button",
"openNewTab": "thisTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.isButton())
.toEqual(true);
});
it("knows when it should open in the same tab", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "link",
"openNewTab": "thisTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.openNewTab())
.toBe(false);
});
it("knows when it is a link", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "link",
"openNewTab": "thisTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.openNewTab())
.toBe(false);
});
});
}
);

View File

@@ -0,0 +1,70 @@
This bundle provides the Timeline domain object type, as well
as other associated domain object types and relevant views.
# Implementation notes
## Model Properties
The properties below record properties relevant to using and
understanding timelines based on their JSON representation.
Additional common properties, such as `modified`
or `persisted` timestamps, may also be present.
### Timeline Model
A timeline's model looks like:
```
{
"type": "timeline",
"start": {
"timestamp": <number> (milliseconds since epoch),
"epoch": <string> (currently, always "SET")
},
"capacity": <number> (optional; battery capacity in watt-hours)
"composition": <string[]> (array of identifiers for contained objects)
}
```
The identifiers in a timeline's `composition` field should refer to
other Timeline objects, or to Activity objects.
### Activity Model
An activity's model looks like:
```
{
"type": "activity",
"start": {
"timestamp": <number> (milliseconds since epoch),
"epoch": <string> (currently, always "SET")
},
"duration": {
"timestamp": <number> (duration of this activity, in milliseconds)
"epoch": "SET" (this is ignored)
},
"relationships": {
"modes": <string[]> (array of applicable Activity Mode ids)
},
"link": <string> (optional; URL linking to associated external resource)
"composition": <string[]> (array of identifiers for contained objects)
}
```
The identifiers in a timeline's `composition` field should only refer to
other Activity objects.
### Activity Mode Model
An activity mode's model looks like:
```
{
"type": "mode",
"resources": {
"comms": <number> (communications utilization, in Kbps)
"power": <number> (power utilization, in watts)
}
}
```

View File

@@ -20,40 +20,33 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import HyperlinkLayout from './HyperlinkLayout.vue';
import Vue from 'vue';
export default function HyperlinkProvider(openmct) {
define([
"./res/templates/deprecated-timeline-message.html"
], function (
deprecatedTimelineMessage
) {
return {
key: 'hyperlink.view',
name: 'Hyperlink',
cssClass: 'icon-chain-links',
canView(domainObject) {
return domainObject.type === 'hyperlink';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
HyperlinkLayout
},
provide: {
domainObject
},
template: '<hyperlink-layout></hyperlink-layout>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
name: 'platform/features/timeline',
definition: {
extensions: {
types: [
{
key: "timeline",
name: "Timeline",
description: "Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported. (07/18/2018)",
priority: 502
}
],
views: [
{
key: "timeline",
name: "Timeline",
type: "timeline",
description: "Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported. (07/18/2018)",
template: deprecatedTimelineMessage
}
]
}
}
};
}
});

View File

@@ -0,0 +1,10 @@
<div>
Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported.
</div>
<div>
Please open an issue in the
<a href="https://github.com/nasa/openmct/issues" target="_blank">
Open MCT Issue tracker
</a>
if you have any questions about the timeline plugin.
</div>

View File

@@ -122,7 +122,6 @@ define([
}
};
this.destroy = this.destroy.bind(this);
/**
* Tracks current selection state of the application.
* @private
@@ -136,7 +135,7 @@ define([
* @memberof module:openmct.MCT#
* @name conductor
*/
this.time = new api.TimeAPI(this);
this.time = new api.TimeAPI();
/**
* An interface for interacting with the composition of domain objects.
@@ -263,7 +262,7 @@ define([
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable.default());
this.install(this.plugins.TelemetryTable());
this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin());
this.install(LicensesPlugin.default());
@@ -284,7 +283,6 @@ define([
this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
this.install(this.plugins.ViewLargeAction());
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.NonEditableFolder());
}
@@ -436,8 +434,6 @@ define([
Browse(this);
}
window.addEventListener('beforeunload', this.destroy);
this.router.start();
this.emit('start');
}.bind(this));
@@ -461,7 +457,6 @@ define([
};
MCT.prototype.destroy = function () {
window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy');
this.router.destroy();
};

View File

@@ -36,7 +36,8 @@ define([
'./views/installLegacyViews',
'./policies/LegacyCompositionPolicyAdapter',
'./actions/LegacyActionAdapter',
'./services/LegacyPersistenceAdapter'
'./services/LegacyPersistenceAdapter',
'./services/ExportImageService'
], function (
ActionDialogDecorator,
AdapterCapability,
@@ -53,7 +54,8 @@ define([
installLegacyViews,
legacyCompositionPolicyAdapter,
LegacyActionAdapter,
LegacyPersistenceAdapter
LegacyPersistenceAdapter,
ExportImageService
) {
return {
name: 'src/adapter',
@@ -82,6 +84,13 @@ define([
"identifierService",
"cacheService"
]
},
{
"key": "exportImageService",
"implementation": ExportImageService,
"depends": [
"dialogService"
]
}
],
components: [

View File

@@ -173,11 +173,10 @@ define([
const limitEvaluator = oldObject.getCapability("limit");
return {
limits: () => {
return limitEvaluator.limits.then !== undefined
? limitEvaluator.limits()
: Promise.resolve(limitEvaluator.limits());
limits: function () {
return limitEvaluator.limits();
}
};
};

View File

@@ -0,0 +1,218 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/**
* Module defining ExportImageService. Created by hudsonfoo on 09/02/16
*/
define(
[
"html2canvas",
"saveAs"
],
function (
html2canvas,
{ saveAs }
) {
/**
* The export image service will export any HTML node to
* JPG, or PNG.
* @param {object} dialogService
* @constructor
*/
function ExportImageService(dialogService) {
this.dialogService = dialogService;
this.exportCount = 0;
}
/**
* Converts an HTML element into a PNG or JPG Blob.
* @private
* @param {node} element that will be converted to an image
* @param {object} options Image options.
* @returns {promise}
*/
ExportImageService.prototype.renderElement = function (element, {imageType, className, thumbnailSize}) {
const self = this;
const dialogService = this.dialogService;
const dialog = dialogService.showBlockingMessage({
title: "Capturing...",
hint: "Capturing an image",
unknownProgress: true,
severity: "info",
delay: true
});
let mimeType = "image/png";
if (imageType === "jpg") {
mimeType = "image/jpeg";
}
let exportId = undefined;
let oldId = undefined;
if (className) {
exportId = 'export-element-' + this.exportCount;
this.exportCount++;
oldId = element.id;
element.id = exportId;
}
return html2canvas(element, {
onclone: function (document) {
if (className) {
const clonedElement = document.getElementById(exportId);
clonedElement.classList.add(className);
}
element.id = oldId;
},
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) {
dialog.dismiss();
return new Promise(function (resolve, reject) {
if (thumbnailSize) {
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
return canvas.toBlob(blob => resolve({
blob,
thumbnail
}), mimeType);
}
return canvas.toBlob(blob => resolve({ blob }), mimeType);
});
}, function (error) {
console.log('error capturing image', error);
dialog.dismiss();
const errorDialog = dialogService.showBlockingMessage({
title: "Error capturing image",
severity: "error",
hint: "Image was not captured successfully!",
options: [{
label: "OK",
callback: function () {
errorDialog.dismiss();
}
}]
});
});
};
ExportImageService.prototype.getThumbnail = function (canvas, mimeType, size) {
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.setAttribute('width', size.width);
thumbnailCanvas.setAttribute('height', size.height);
const ctx = thumbnailCanvas.getContext('2d');
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
return thumbnailCanvas.toDataURL(mimeType);
};
/**
* Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
ExportImageService.prototype.exportJPG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, {
imageType: 'jpg',
className
})
.then(function (img) {
saveAs(img.blob, processedFilename);
});
};
/**
* Takes a screenshot of a DOM node and exports to PNG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
ExportImageService.prototype.exportPNG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, {
imageType: 'png',
className
})
.then(function (img) {
saveAs(img.blob, processedFilename);
});
};
/**
* Takes a screenshot of a DOM node in PNG format.
* @param {node} element to be exported
* @param {string} filename the exported image
* @returns {promise}
*/
ExportImageService.prototype.exportPNGtoSRC = function (element, options) {
return this.renderElement(element, {
imageType: 'png',
...options
});
};
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
return filename.replace(regex, '_');
}
/**
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
* implements the method in browsers that would not otherwise support it.
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
*/
function polyfillToBlob() {
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", {
value: function (callback, mimeType, quality) {
const binStr = atob(this.toDataURL(mimeType, quality).split(',')[1]);
const len = binStr.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], {type: mimeType || "image/png"}));
}
});
}
}
polyfillToBlob();
return ExportImageService;
}
);

View File

@@ -46,6 +46,8 @@ class ActionCollection extends EventEmitter {
this._observeObjectPath();
this.openmct.editor.on('isEditing', this._updateActions);
}
this._initializeActions();
}
disable(actionKeys) {
@@ -154,10 +156,19 @@ class ActionCollection extends EventEmitter {
});
}
_initializeActions() {
Object.keys(this.applicableActions).forEach(key => {
this.applicableActions[key].callBack = () => {
return this.applicableActions[key].invoke(this.objectPath, this.view);
};
});
}
_updateActions() {
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
this._initializeActions();
this._update();
}

View File

@@ -34,7 +34,7 @@ class ActionsAPI extends EventEmitter {
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
this.register = this.register.bind(this);
this.getActionsCollection = this.getActionsCollection.bind(this);
this.get = this.get.bind(this);
this._applicableActions = this._applicableActions.bind(this);
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
}
@@ -43,14 +43,12 @@ class ActionsAPI extends EventEmitter {
this._allActions[actionDefinition.key] = actionDefinition;
}
getAction(key) {
return this._allActions[key];
}
getActionsCollection(objectPath, view) {
get(objectPath, view) {
if (view) {
return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
} else {
return this._newActionCollection(objectPath, view, true);
}
}
@@ -59,6 +57,15 @@ class ActionsAPI extends EventEmitter {
this._groupOrder = groupArray;
}
_get(objectPath, view) {
let actionCollection = this._newActionCollection(objectPath, view);
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
return actionCollection;
}
_getCachedActionCollection(objectPath, view) {
let cachedActionCollection = this._actionCollections.get(view);
@@ -68,17 +75,7 @@ class ActionsAPI extends EventEmitter {
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
let applicableActions = this._applicableActions(objectPath, view);
const actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
if (view) {
this._cacheActionCollection(view, actionCollection);
}
return actionCollection;
}
_cacheActionCollection(view, actionCollection) {
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
}
_updateCachedActionCollections(key) {

View File

@@ -106,7 +106,7 @@ describe('The Actions API', () => {
it("adds action to ActionsAPI", () => {
actionsAPI.register(mockAction);
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);
@@ -121,21 +121,21 @@ describe('The Actions API', () => {
});
it("returns an ActionCollection when invoked with an objectPath only", () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
let actionCollection = actionsAPI.get(mockObjectPath);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue();
});
it("returns an ActionCollection when invoked with an objectPath and view", () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue();
});
it("returns relevant actions when invoked with objectPath only", () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
let actionCollection = actionsAPI.get(mockObjectPath);
let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
expect(action.key).toEqual(mockObjectPathAction.key);
@@ -143,7 +143,7 @@ describe('The Actions API', () => {
});
it("returns relevant actions when invoked with objectPath and view", () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);

View File

@@ -46,7 +46,7 @@ define([
StatusAPI
) {
return {
TimeAPI: TimeAPI.default,
TimeAPI: TimeAPI,
ObjectAPI: ObjectAPI,
CompositionAPI: CompositionAPI,
TypeRegistry: TypeRegistry,

View File

@@ -37,7 +37,7 @@ import Menu, { MENU_PLACEMENT } from './menu.js';
* @property {Boolean} isDisabled adds disable class if true
* @property {String} name Menu item text
* @property {String} description Menu item description
* @property {Function} onItemClicked callback function: invoked when item is clicked
* @property {Function} callBack callback function: invoked when item is clicked
*/
/**
@@ -66,27 +66,12 @@ class MenuAPI {
* @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions{@link Action} or collection of groups of actions {@link Action}
* @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu
*/
showMenu(x, y, items, menuOptions) {
this._createMenuComponent(x, y, items, menuOptions);
showMenu(x, y, actions, menuOptions) {
this._createMenuComponent(x, y, actions, menuOptions);
this.menuComponent.showMenu();
}
actionsToMenuItems(actions, objectPath, view) {
return actions.map(action => {
const isActionGroup = Array.isArray(action);
if (isActionGroup) {
action = this.actionsToMenuItems(action, objectPath, view);
} else {
action.onItemClicked = () => {
action.invoke(objectPath, view);
};
}
return action;
});
}
/**
* Show popup menu with description of item on hover
* @param {number} x x-coordinates for popup

View File

@@ -57,7 +57,7 @@ describe ('The Menu API', () => {
name: 'Test Action 1',
cssClass: 'icon-clock',
description: 'This is a test action',
onItemClicked: () => {
callBack: () => {
result = 'Test Action 1 Invoked';
}
},
@@ -66,7 +66,7 @@ describe ('The Menu API', () => {
name: 'Test Action 2',
cssClass: 'icon-clock',
description: 'This is a test action',
onItemClicked: () => {
callBack: () => {
result = 'Test Action 2 Invoked';
}
}

View File

@@ -11,7 +11,7 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
@click="action.onItemClicked"
@click="action.callBack"
>
{{ action.name }}
</li>
@@ -36,7 +36,7 @@
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.onItemClicked"
@click="action.callBack"
>
{{ action.name }}
</li>

View File

@@ -13,7 +13,7 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
@click="action.onItemClicked"
@click="action.callBack"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
@@ -42,7 +42,7 @@
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.onItemClicked"
@click="action.callBack"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>

View File

@@ -71,12 +71,12 @@ class Menu extends EventEmitter {
showMenu() {
this.component = new Vue({
components: {
MenuComponent
},
provide: {
options: this.options
},
components: {
MenuComponent
},
template: '<menu-component />'
});
@@ -85,12 +85,12 @@ class Menu extends EventEmitter {
showSuperMenu() {
this.component = new Vue({
components: {
SuperMenuComponent
},
provide: {
options: this.options
},
components: {
SuperMenuComponent
},
template: '<super-menu-component />'
});

View File

@@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { TelemetryCollection } = require("./TelemetryCollection");
define([
'../../plugins/displayLayout/CustomStringFormatter',
'./TelemetryMetadataManager',
@@ -275,28 +273,6 @@ 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 requestTelemetryCollection
* @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.requestTelemetryCollection = 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

@@ -1,366 +0,0 @@
/*****************************************************************************
* 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';
/** 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) {
throw new Error('Telemetry Collection has already been 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.abortSignal = 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;
throw new 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(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) {
this.timeKey = timeSystem.key;
let metadataValue = this.metadata.value(this.timeKey) || { format: this.timeKey };
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);
}
}

View File

@@ -1,180 +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 TimeContext from "./TimeContext";
class GlobalTimeContext extends TimeContext {
constructor() {
super();
//The Time Of Interest
this.toi = undefined;
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
/**
* Get or set the Time of Interest. The Time of Interest is a single point
* in time, and constitutes the temporal focus of application views. It can
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
timeOfInterest(newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
return this.toi;
}
}
export default GlobalTimeContext;

View File

@@ -1,141 +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 TimeContext from "./TimeContext";
class IndependentTimeContext extends TimeContext {
constructor(globalTimeContext, key) {
super();
this.key = key;
this.tick = this.tick.bind(this);
this.globalTimeContext = globalTimeContext;
this.globalTimeContext.on('tick', this.tick);
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
if (!this.activeClock) {
return;
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
}
export default IndependentTimeContext;

View File

@@ -20,35 +20,51 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import GlobalTimeContext from "./GlobalTimeContext";
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
define(['EventEmitter'], function (EventEmitter) {
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
* one {@link TimeSystem}, as well as some default time bounds must be
* registered and enabled via {@link TimeAPI.addTimeSystem} and
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
*
* Time-sensitive views will typically respond to changes to bounds or other
* properties of the time conductor and update the data displayed based on
* the temporal state of the application. The current time bounds are also
* used in queries for historical data.
*
* The TimeAPI extends the EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
*/
function TimeAPI() {
EventEmitter.call(this);
//The Time System
this.system = undefined;
//The Time Of Interest
this.toi = undefined;
this.boundsVal = {
start: undefined,
end: undefined
};
this.timeSystems = new Map();
this.clocks = new Map();
this.activeClock = undefined;
this.offsets = undefined;
this.tick = this.tick.bind(this);
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
* one {@link TimeSystem}, as well as some default time bounds must be
* registered and enabled via {@link TimeAPI.addTimeSystem} and
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
*
* Time-sensitive views will typically respond to changes to bounds or other
* properties of the time conductor and update the data displayed based on
* the temporal state of the application. The current time bounds are also
* used in queries for historical data.
*
* The TimeAPI extends the EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
*/
class TimeAPI extends GlobalTimeContext {
constructor(openmct) {
super();
this.openmct = openmct;
this.independentContexts = new Map();
}
TimeAPI.prototype = Object.create(EventEmitter.prototype);
/**
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
* MCT supports multiple different types of time values, although all are
@@ -78,16 +94,16 @@ class TimeAPI extends GlobalTimeContext {
* @memberof module:openmct.TimeAPI#
* @param {TimeSystem} timeSystem A time system object.
*/
addTimeSystem(timeSystem) {
TimeAPI.prototype.addTimeSystem = function (timeSystem) {
this.timeSystems.set(timeSystem.key, timeSystem);
}
};
/**
* @returns {TimeSystem[]}
*/
getAllTimeSystems() {
TimeAPI.prototype.getAllTimeSystems = function () {
return Array.from(this.timeSystems.values());
}
};
/**
* Clocks provide a timing source that is used to
@@ -110,80 +126,340 @@ class TimeAPI extends GlobalTimeContext {
* @memberof module:openmct.TimeAPI#
* @param {Clock} clock
*/
addClock(clock) {
TimeAPI.prototype.addClock = function (clock) {
this.clocks.set(clock.key, clock);
}
};
/**
* @memberof module:openmct.TimeAPI#
* @returns {Clock[]}
* @memberof module:openmct.TimeAPI#
*/
getAllClocks() {
TimeAPI.prototype.getAllClocks = function () {
return Array.from(this.clocks.values());
}
};
/**
* Get or set an independent time observer which follows the TimeAPI timeSystem,
* but with different offsets for a given domain object
* @param {key | string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {key | string} clockKey the real time clock key currently in use
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @returns {string | true} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method addIndependentTimeContext
* @method validateBounds
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.independentContexts.get(key);
if (!timeContext) {
timeContext = new IndependentTimeContext(this, key);
this.independentContexts.set(key, timeContext);
TimeAPI.prototype.validateBounds = function (bounds) {
if ((bounds.start === undefined)
|| (bounds.end === undefined)
|| isNaN(bounds.start)
|| isNaN(bounds.end)
) {
return "Start and end must be specified as integer values";
} else if (bounds.start > bounds.end) {
return "Specified start date exceeds end bound";
}
if (clockKey) {
timeContext.clock(clockKey, value);
} else {
timeContext.bounds(value);
return true;
};
/**
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns {string | true} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
TimeAPI.prototype.validateOffsets = function (offsets) {
if ((offsets.start === undefined)
|| (offsets.end === undefined)
|| isNaN(offsets.start)
|| isNaN(offsets.end)
) {
return "Start and end offsets must be specified as integer values";
} else if (offsets.start >= offsets.end) {
return "Specified start offset must be < end offset";
}
this.emit('timeContext', key);
return () => {
this.independentContexts.delete(key);
timeContext.emit('timeContext', key);
};
}
return true;
};
/**
* Get the independent time observer which follows the TimeAPI timeSystem,
* but with different offsets.
* @param {key | string} key The identifier key of the domain object these offsets
* @memberof module:openmct.TimeAPI#
* @method getIndependentTimeContext
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
getIndependentContext(key) {
return this.independentContexts.get(key);
}
/**
* Get the timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
* Otherwise, the global time context will be returned.
* @param { Array } objectPath The view's objectPath
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method getContextForView
* @method bounds
*/
getContextForView(objectPath) {
let timeContext = this;
objectPath.forEach(item => {
const key = this.openmct.objects.makeKeyString(item.identifier);
if (this.independentContexts.get(key)) {
timeContext = this.independentContexts.get(key);
TimeAPI.prototype.bounds = function (newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult !== true) {
throw new Error(validationResult);
}
});
return timeContext;
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
}
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
export default TimeAPI;
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
};
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystem
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
*/
TimeAPI.prototype.timeSystem = function (timeSystemOrKey, bounds) {
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
"Must specify bounds when changing time system without "
+ "an active clock."
);
}
let timeSystem;
if (timeSystemOrKey === undefined) {
throw "Please provide a time system";
}
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
}
} else {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
}
return this.system;
};
/**
* Get or set the Time of Interest. The Time of Interest is a single point
* in time, and constitutes the temporal focus of application views. It can
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
TimeAPI.prototype.timeOfInterest = function (newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
return this.toi;
};
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which boudns will be calculated
* using current offsets.
*/
TimeAPI.prototype.tick = function (timestamp) {
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
};
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
TimeAPI.prototype.clock = function (keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
};
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
TimeAPI.prototype.clockOffsets = function (offsets) {
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
if (validationResult !== true) {
throw new Error(validationResult);
}
this.offsets = offsets;
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.bounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit("clockOffsets", offsets);
}
return this.offsets;
};
/**
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
TimeAPI.prototype.stopClock = function () {
if (this.activeClock) {
this.clock(undefined, undefined);
}
};
return TimeAPI;
});

View File

@@ -19,243 +19,241 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeAPI from "./TimeAPI";
import {createOpenMct} from "utils/testing";
describe("The Time API", function () {
let api;
let timeSystemKey;
let timeSystem;
let clockKey;
let clock;
let bounds;
let eventListener;
let toi;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
timeSystemKey = "timeSystemKey";
timeSystem = {key: timeSystemKey};
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
bounds = {
start: 0,
end: 1
};
eventListener = jasmine.createSpy("eventListener");
toi = 111;
});
it("Supports setting and querying of time of interest", function () {
expect(api.timeOfInterest()).not.toBe(toi);
api.timeOfInterest(toi);
expect(api.timeOfInterest()).toBe(toi);
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
expect(api.bounds()).not.toBe(bounds);
expect(api.bounds.bind(api, bounds)).not.toThrow();
expect(api.bounds()).toEqual(bounds);
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
});
it("Allows setting of previously registered time system with bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Disallows setting of time system without bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).toThrow();
expect(api.timeSystem()).not.toBe(timeSystem);
});
it("allows setting of timesystem without bounds with clock", function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
api.clock(clockKey, {
start: 0,
end: 1
});
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Emits an event when time system changes", function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on("timeSystem", eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it("Emits an event when time of interest changes", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("timeOfInterest", eventListener);
api.timeOfInterest(toi);
expect(eventListener).toHaveBeenCalledWith(toi);
});
it("Emits an event when bounds change", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("bounds", eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
api.timeOfInterest(6);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toEqual(6);
});
it("If bounds are set and TOI lies outside them, reset TOI", function () {
api.timeOfInterest(11);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toBeUndefined();
});
it("Maintains delta during tick", function () {
});
it("Allows registered time system to be activated", function () {
});
it("Allows a registered tick source to be activated", function () {
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
"on",
"off",
"currentValue"
]);
mockTickSource.key = 'mockTickSource';
});
describe(" when enabling a tick source", function () {
let mockTickSource;
let anotherMockTickSource;
const mockOffsets = {
start: 0,
end: 1
};
define(['./TimeAPI'], function (TimeAPI) {
describe("The Time API", function () {
let api;
let timeSystemKey;
let timeSystem;
let clockKey;
let clock;
let bounds;
let eventListener;
let toi;
beforeEach(function () {
mockTickSource = jasmine.createSpyObj("clock", [
api = new TimeAPI();
timeSystemKey = "timeSystemKey";
timeSystem = {key: timeSystemKey};
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(10);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
bounds = {
start: 0,
end: 1
};
eventListener = jasmine.createSpy("eventListener");
toi = 111;
});
it("Supports setting and querying of time of interest", function () {
expect(api.timeOfInterest()).not.toBe(toi);
api.timeOfInterest(toi);
expect(api.timeOfInterest()).toBe(toi);
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
expect(api.bounds()).not.toBe(bounds);
expect(api.bounds.bind(api, bounds)).not.toThrow();
expect(api.bounds()).toEqual(bounds);
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
});
it("Allows setting of previously registered time system with bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Disallows setting of time system without bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).toThrow();
expect(api.timeSystem()).not.toBe(timeSystem);
});
it("allows setting of timesystem without bounds with clock", function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
api.clock(clockKey, {
start: 0,
end: 1
});
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Emits an event when time system changes", function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on("timeSystem", eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it("Emits an event when time of interest changes", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("timeOfInterest", eventListener);
api.timeOfInterest(toi);
expect(eventListener).toHaveBeenCalledWith(toi);
});
it("Emits an event when bounds change", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("bounds", eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
api.timeOfInterest(6);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toEqual(6);
});
it("If bounds are set and TOI lies outside them, reset TOI", function () {
api.timeOfInterest(11);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toBeUndefined();
});
it("Maintains delta during tick", function () {
});
it("Allows registered time system to be activated", function () {
});
it("Allows a registered tick source to be activated", function () {
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
"on",
"off",
"currentValue"
]);
mockTickSource.key = 'mockTickSource';
});
describe(" when enabling a tick source", function () {
let mockTickSource;
let anotherMockTickSource;
const mockOffsets = {
start: 0,
end: 1
};
beforeEach(function () {
mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(10);
mockTickSource.key = "mts";
anotherMockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
anotherMockTickSource.key = "amts";
anotherMockTickSource.currentValue.and.returnValue(10);
api.addClock(mockTickSource);
api.addClock(anotherMockTickSource);
});
it("sets bounds based on current value", function () {
api.clock("mts", mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
end: 11
});
});
it("a new tick listener is registered", function () {
api.clock("mts", mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("listener of existing tick source is reregistered", function () {
api.clock("mts", mockOffsets);
api.clock("amts", mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("Allows the active clock to be set and unset", function () {
expect(api.clock()).toBeUndefined();
api.clock("mts", mockOffsets);
expect(api.clock()).toBeDefined();
api.stopClock();
expect(api.clock()).toBeUndefined();
});
});
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
const mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(100);
let tickCallback;
const boundsCallback = jasmine.createSpy("boundsCallback");
const clockOffsets = {
start: -100,
end: 100
};
mockTickSource.key = "mts";
anotherMockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
anotherMockTickSource.key = "amts";
anotherMockTickSource.currentValue.and.returnValue(10);
api.addClock(mockTickSource);
api.addClock(anotherMockTickSource);
api.clock("mts", clockOffsets);
api.on("bounds", boundsCallback);
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(1000);
expect(boundsCallback).toHaveBeenCalledWith({
start: 900,
end: 1100
}, true);
});
it("sets bounds based on current value", function () {
api.clock("mts", mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
end: 11
});
});
it("a new tick listener is registered", function () {
api.clock("mts", mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("listener of existing tick source is reregistered", function () {
api.clock("mts", mockOffsets);
api.clock("amts", mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("Allows the active clock to be set and unset", function () {
expect(api.clock()).toBeUndefined();
api.clock("mts", mockOffsets);
expect(api.clock()).toBeDefined();
api.stopClock();
expect(api.clock()).toBeUndefined();
});
});
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
const mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(100);
let tickCallback;
const boundsCallback = jasmine.createSpy("boundsCallback");
const clockOffsets = {
start: -100,
end: 100
};
mockTickSource.key = "mts";
api.addClock(mockTickSource);
api.clock("mts", clockOffsets);
api.on("bounds", boundsCallback);
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(1000);
expect(boundsCallback).toHaveBeenCalledWith({
start: 900,
end: 1100
}, true);
});
});

View File

@@ -1,250 +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 EventEmitter from 'EventEmitter';
class TimeContext extends EventEmitter {
constructor() {
super();
//The Time System
this.timeSystems = new Map();
this.system = undefined;
this.clocks = new Map();
this.boundsVal = {
start: undefined,
end: undefined
};
this.activeClock = undefined;
this.offsets = undefined;
this.tick = this.tick.bind(this);
}
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystem
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
"Must specify bounds when changing time system without "
+ "an active clock."
);
}
let timeSystem;
if (timeSystemOrKey === undefined) {
throw "Please provide a time system";
}
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
}
} else {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
}
return this.system;
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @returns {ValidationResult} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
validateBounds(bounds) {
if ((bounds.start === undefined)
|| (bounds.end === undefined)
|| isNaN(bounds.start)
|| isNaN(bounds.end)
) {
return {
valid: false,
message: "Start and end must be specified as integer values"
};
} else if (bounds.start > bounds.end) {
return {
valid: false,
message: "Specified start date exceeds end bound"
};
}
return {
valid: true,
message: ''
};
}
/**
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns { ValidationResult } A validation error, and true/false if valid or not
* @memberof module:openmct.TimeAPI#
* @method validateOffsets
*/
validateOffsets(offsets) {
if ((offsets.start === undefined)
|| (offsets.end === undefined)
|| isNaN(offsets.start)
|| isNaN(offsets.end)
) {
return {
valid: false,
message: "Start and end offsets must be specified as integer values"
};
} else if (offsets.start >= offsets.end) {
return {
valid: false,
message: "Specified start offset must be < end offset"
};
}
return {
valid: true,
message: ''
};
}
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
this.offsets = offsets;
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.bounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit("clockOffsets", offsets);
}
return this.offsets;
}
/**
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
if (this.activeClock) {
this.clock(undefined, undefined);
}
}
}
export default TimeContext;

View File

@@ -1,155 +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 TimeAPI from "./TimeAPI";
import {createOpenMct} from "utils/testing";
describe("The Independent Time API", function () {
let api;
let domainObjectKey;
let clockKey;
let clock;
let bounds;
let independentBounds;
let eventListener;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
api.addClock(clock);
domainObjectKey = 'test-key';
bounds = {
start: 0,
end: 1
};
api.bounds(bounds);
independentBounds = {
start: 10,
end: 11
};
eventListener = jasmine.createSpy("eventListener");
});
it("Creates an independent time context", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getIndependentContext(domainObjectKey);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("Gets an independent time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}, { identifier: domainObjectKey }]);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("defaults to the global time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}]);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(timeContext.bounds()).not.toEqual(bounds);
timeContext.bounds(bounds);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(timeContext.bounds()).not.toBe(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(timeContext.bounds()).not.toEqual(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
destroyTimeContext();
});
it("Emits an event when bounds change", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
destroyTimeContext();
});
describe(" when using real time clock", function () {
const mockOffsets = {
start: 10,
end: 11
};
it("Emits an event when bounds change based on current value", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.clock('someClockKey', mockOffsets);
timeContext.on('bounds', eventListener);
timeContext.tick(10);
expect(eventListener).toHaveBeenCalledWith({
start: 20,
end: 21
}, true);
destroyTimeContext();
});
});
});

View File

@@ -1,188 +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.
*****************************************************************************/
/**
* Class defining an image exporter for JPG/PNG output.
* Originally created by hudsonfoo on 09/02/16
*/
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
return filename.replace(regex, '_');
}
import {saveAs} from 'file-saver/FileSaver';
import html2canvas from 'html2canvas';
import uuid from 'uuid';
class ImageExporter {
constructor(openmct) {
this.openmct = openmct;
}
/**
* Converts an HTML element into a PNG or JPG Blob.
* @private
* @param {node} element that will be converted to an image
* @param {object} options Image options.
* @returns {promise}
*/
renderElement(element, { imageType, className, thumbnailSize }) {
const self = this;
const overlays = this.openmct.overlays;
const dialog = overlays.dialog({
iconClass: 'info',
message: 'Caputuring an image',
buttons: [
{
label: 'Cancel',
emphasis: true,
callback: function () {
dialog.dismiss();
}
}
]
});
let mimeType = 'image/png';
if (imageType === 'jpg') {
mimeType = 'image/jpeg';
}
let exportId = undefined;
let oldId = undefined;
if (className) {
const newUUID = uuid();
exportId = `$export-element-${newUUID}`;
oldId = element.id;
element.id = exportId;
}
return html2canvas(element, {
useCORS: true,
allowTaint: true,
logging: false,
onclone: function (document) {
if (className) {
const clonedElement = document.getElementById(exportId);
clonedElement.classList.add(className);
}
element.id = oldId;
},
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) {
dialog.dismiss();
return new Promise(function (resolve, reject) {
if (thumbnailSize) {
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
return canvas.toBlob(blob => resolve({
blob,
thumbnail
}), mimeType);
}
return canvas.toBlob(blob => resolve({ blob }), mimeType);
});
}, function (error) {
console.log('error capturing image', error);
dialog.dismiss();
const errorDialog = overlays.dialog({
iconClass: 'error',
message: 'Image was not captured successfully!',
buttons: [
{
label: "OK",
emphasis: true,
callback: function () {
errorDialog.dismiss();
}
}
]
});
});
}
getThumbnail(canvas, mimeType, size) {
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.setAttribute('width', size.width);
thumbnailCanvas.setAttribute('height', size.height);
const ctx = thumbnailCanvas.getContext('2d');
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
return thumbnailCanvas.toDataURL(mimeType);
}
/**
* Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
async exportJPG(element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
const img = await this.renderElement(element, {
imageType: 'jpg',
className
});
saveAs(img.blob, processedFilename);
}
/**
* Takes a screenshot of a DOM node and exports to PNG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
async exportPNG(element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
const img = await this.renderElement(element, {
imageType: 'png',
className
});
saveAs(img.blob, processedFilename);
}
/**
* Takes a screenshot of a DOM node in PNG format.
* @param {node} element to be exported
* @param {string} filename the exported image
* @returns {promise}
*/
exportPNGtoSRC(element, options) {
return this.renderElement(element, {
imageType: 'png',
...options
});
}
}
export default ImageExporter;

View File

@@ -1,58 +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 ImageExporter from './ImageExporter';
import { createOpenMct, resetApplicationState } from '../utils/testing';
describe('The Image Exporter', () => {
let openmct;
let imageExporter;
beforeEach(() => {
openmct = createOpenMct();
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("basic instatation", () => {
it("can be instatiated", () => {
imageExporter = new ImageExporter(openmct);
expect(imageExporter).not.toEqual(null);
});
it("can render an element to a blob", async () => {
const mockHeadElement = document.createElement("h1");
const mockTextNode = document.createTextNode('foo bar');
mockHeadElement.appendChild(mockTextNode);
document.body.appendChild(mockHeadElement);
imageExporter = new ImageExporter(openmct);
const returnedBlob = await imageExporter.renderElement(document.body, {
imageType: 'png'
});
expect(returnedBlob).not.toEqual(null);
expect(returnedBlob.blob).not.toEqual(null);
expect(returnedBlob.blob).toBeInstanceOf(Blob);
});
});
});

View File

@@ -38,6 +38,8 @@ const DEFAULTS = [
'platform/exporters',
'platform/telemetry',
'platform/features/clock',
'platform/features/hyperlink',
'platform/features/timeline',
'platform/forms',
'platform/identity',
'platform/persistence/aggregator',
@@ -80,7 +82,9 @@ define([
'../platform/exporters/bundle',
'../platform/features/clock/bundle',
'../platform/features/my-items/bundle',
'../platform/features/hyperlink/bundle',
'../platform/features/static-markup/bundle',
'../platform/features/timeline/bundle',
'../platform/forms/bundle',
'../platform/framework/bundle',
'../platform/framework/src/load/Bundle',

View File

@@ -19,8 +19,8 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import LadTableSetView from './LadTableSetView';
import LadTableSet from './components/LadTableSet.vue';
import Vue from 'vue';
export default function LADTableSetViewProvider(openmct) {
return {
@@ -34,7 +34,32 @@ export default function LADTableSetViewProvider(openmct) {
return domainObject.type === 'LadTableSet';
},
view: function (domainObject, objectPath) {
return new LadTableSetView(openmct, domainObject, objectPath);
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
LadTableSet: LadTableSet
},
provide: {
openmct,
objectPath
},
data() {
return {
domainObject
};
},
template: '<lad-table-set :domain-object="domainObject"></lad-table-set>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;

View File

@@ -1,45 +0,0 @@
import LadTable from './components/LADTable.vue';
import Vue from 'vue';
export default class LADTableView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
components: {
LadTable
},
provide: {
openmct: this.openmct,
currentView: this
},
data: () => {
return {
domainObject: this.domainObject,
objectPath: this.objectPath
};
},
template: '<lad-table ref="ladTable" :domain-object="domainObject" :object-path="objectPath"></lad-table>'
});
}
getViewContext() {
if (!this.component) {
return {};
}
return this.component.$refs.ladTable.getViewContext();
}
destroy(element) {
this.component.$destroy();
this.component = undefined;
}
}

View File

@@ -19,30 +19,50 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import LadTable from './components/LADTable.vue';
import Vue from 'vue';
import LADTableView from './LADTableView';
export default function LADTableViewProvider(openmct) {
return {
key: 'LadTable',
name: 'LAD Table',
cssClass: 'icon-tabular-lad',
canView: function (domainObject) {
return domainObject.type === 'LadTable';
},
canEdit: function (domainObject) {
return domainObject.type === 'LadTable';
},
view: function (domainObject, objectPath) {
let component;
export default class LADTableViewProvider {
constructor(openmct) {
this.openmct = openmct;
this.name = 'LAD Table';
this.key = 'LadTable';
this.cssClass = 'icon-tabular-lad';
}
canView(domainObject) {
return domainObject.type === 'LadTable';
}
canEdit(domainObject) {
return domainObject.type === 'LadTable';
}
view(domainObject, objectPath) {
return new LADTableView(this.openmct, domainObject, objectPath);
}
priority(domainObject) {
return 1;
}
return {
show: function (element) {
component = new Vue({
el: element,
components: {
LadTableComponent: LadTable
},
provide: {
openmct
},
data: () => {
return {
domainObject,
objectPath
};
},
template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -1,45 +0,0 @@
import LadTableSet from './components/LadTableSet.vue';
import Vue from 'vue';
export default class LadTableSetView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
components: {
LadTableSet
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
currentView: this
},
data: () => {
return {
domainObject: this.domainObject
};
},
template: '<lad-table-set ref="ladTableSet" :domain-object="domainObject"></lad-table-set>'
});
}
getViewContext() {
if (!this.component) {
return {};
}
return this.component.$refs.ladTableSet.getViewContext();
}
destroy(element) {
this.component.$destroy();
this.component = undefined;
}
}

View File

@@ -50,7 +50,7 @@ const CONTEXT_MENU_ACTIONS = [
];
export default {
inject: ['openmct', 'currentView'],
inject: ['openmct'],
props: {
domainObject: {
type: Object,
@@ -167,23 +167,25 @@ export default {
this.resetValues();
this.timestampKey = timeSystem.key;
},
updateViewContext() {
this.$emit('rowContextClick', {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: () => {
return this.datum;
getView() {
return {
getViewContext: () => {
return {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: () => {
return this.datum;
}
};
}
});
};
},
showContextMenu(event) {
this.updateViewContext();
let actionCollection = this.openmct.actions.get(this.objectPath, this.getView());
let allActions = actionCollection.getActionsObject();
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
const actions = CONTEXT_MENU_ACTIONS.map(key => this.openmct.actions.getAction(key));
const menuItems = this.openmct.menus.actionsToMenuItems(actions, this.objectPath, this.currentView);
if (menuItems.length) {
this.openmct.menus.showMenu(event.x, event.y, menuItems);
}
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
},
resetValues() {
this.value = '---';

View File

@@ -38,7 +38,6 @@
:domain-object="ladRow.domainObject"
:path-to-table="objectPath"
:has-units="hasUnits"
@rowContextClick="updateViewContext"
/>
</tbody>
</table>
@@ -52,7 +51,7 @@ export default {
components: {
LadRow
},
inject: ['openmct', 'currentView'],
inject: ['openmct'],
props: {
domainObject: {
type: Object,
@@ -65,8 +64,7 @@ export default {
},
data() {
return {
items: [],
viewContext: {}
items: []
};
},
computed: {
@@ -116,12 +114,6 @@ export default {
let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
return metadataWithUnits.length > 0;
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},
getViewContext() {
return this.viewContext;
}
}
};

View File

@@ -48,7 +48,6 @@
:domain-object="ladRow.domainObject"
:path-to-table="ladTable.objectPath"
:has-units="hasUnits"
@rowContextClick="updateViewContext"
/>
</template>
</tbody>
@@ -62,7 +61,7 @@ export default {
components: {
LadRow
},
inject: ['openmct', 'objectPath', 'currentView'],
inject: ['openmct', 'objectPath'],
props: {
domainObject: {
type: Object,
@@ -73,8 +72,7 @@ export default {
return {
ladTableObjects: [],
ladTelemetryObjects: {},
compositions: [],
viewContext: {}
compositions: []
};
},
computed: {
@@ -168,12 +166,6 @@ export default {
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
};
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},
getViewContext() {
return this.viewContext;
}
}
};

View File

@@ -45,7 +45,6 @@ export default class URLTimeSettingsSynchronizer {
}
initialize() {
this.updateTimeSettings();
this.openmct.router.on('change:params', this.updateTimeSettings);
TIME_EVENTS.forEach(event => {

View File

@@ -41,7 +41,7 @@ export default class ConditionManager extends EventEmitter {
this.subscriptions = {};
this.telemetryObjects = {};
this.testData = {
conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData,
conditionTestData: [],
applied: false
};
this.initialize();
@@ -154,10 +154,8 @@ export default class ConditionManager extends EventEmitter {
updateConditionDescription(condition) {
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
if (found.summary !== condition.description) {
found.summary = condition.description;
this.persistConditions();
}
found.summary = condition.description;
this.persistConditions();
}
initCondition(conditionConfiguration, index) {
@@ -416,10 +414,8 @@ export default class ConditionManager extends EventEmitter {
}
updateTestData(testData) {
if (!_.isEqual(testData, this.testData)) {
this.testData = testData;
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs);
}
this.testData = testData;
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs);
}
persistConditions() {

View File

@@ -215,8 +215,7 @@ export default {
},
isEditing: {
type: Boolean,
required: true,
default: false
required: true
},
telemetry: {
type: Array,

View File

@@ -20,78 +20,71 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import AlphanumericFormat from './components/AlphanumericFormat.vue';
define([
'./components/AlphanumericFormatView.vue',
'vue'
], function (AlphanumericFormatView, Vue) {
import Vue from 'vue';
function AlphanumericFormatViewProvider(openmct, options) {
function isTelemetryObject(selectionPath) {
let selectedObject = selectionPath[0].context.item;
let parentObject = selectionPath[1].context.item;
let selectedLayoutItem = selectionPath[0].context.layoutItem;
class AlphanumericFormatView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
name: 'AlphanumericFormat',
components: {
AlphanumericFormat
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
currentView: this
},
template: '<alphanumeric-format ref="alphanumericFormat"></alphanumeric-format>'
});
}
getViewContext() {
if (this.component) {
return {};
return parentObject
&& parentObject.type === 'layout'
&& selectedObject
&& selectedLayoutItem
&& selectedLayoutItem.type === 'telemetry-view'
&& openmct.telemetry.isTelemetryObject(selectedObject)
&& !options.showAsView.includes(selectedObject.type);
}
return this.component.$refs.alphanumericFormat.getViewContext();
}
return {
key: 'alphanumeric-format',
name: 'Alphanumeric Format',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 1) {
return false;
}
destroy() {
this.component.$destroy();
this.component = undefined;
}
}
return selection.every(isTelemetryObject);
},
view: function (domainObject, objectPath) {
let component;
export default function AlphanumericFormatViewProvider(openmct, options) {
function isTelemetryObject(selectionPath) {
let selectedObject = selectionPath[0].context.item;
let parentObject = selectionPath[1].context.item;
let selectedLayoutItem = selectionPath[0].context.layoutItem;
return parentObject
&& parentObject.type === 'layout'
&& selectedObject
&& selectedLayoutItem
&& selectedLayoutItem.type === 'telemetry-view'
&& openmct.telemetry.isTelemetryObject(selectedObject)
&& !options.showAsView.includes(selectedObject.type);
}
return {
key: 'alphanumeric-format',
name: 'Alphanumeric Format',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 1) {
return false;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
AlphanumericFormatView: AlphanumericFormatView.default
},
provide: {
openmct,
objectPath
},
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
});
},
getViewContext() {
if (component) {
return component.$refs.alphanumericFormatView.getViewContext();
} else {
return {};
}
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}
return selection.every(isTelemetryObject);
},
view: function (domainObject, objectPath) {
return new AlphanumericFormatView(openmct, domainObject, objectPath);
},
priority: function () {
return 1;
}
};
}
return AlphanumericFormatViewProvider;
});

View File

@@ -14,7 +14,7 @@ export default class CopyToClipboardAction {
invoke(objectPath, view = {}) {
const viewContext = view.getViewContext && view.getViewContext();
const formattedValue = viewContext.row.formattedValueForCopy();
const formattedValue = viewContext.formattedValueForCopy();
clipboard.updateClipboard(formattedValue)
.then(() => {
@@ -26,13 +26,9 @@ export default class CopyToClipboardAction {
}
appliesTo(objectPath, view = {}) {
const viewContext = view.getViewContext && view.getViewContext();
const row = viewContext && viewContext.row;
if (!row) {
return false;
}
let viewContext = view.getViewContext && view.getViewContext();
return row.formattedValueForCopy
&& typeof row.formattedValueForCopy === 'function';
return viewContext && viewContext.formattedValueForCopy
&& typeof viewContext.formattedValueForCopy === 'function';
}
}

View File

@@ -52,8 +52,7 @@
<script>
export default {
name: 'AlphanumericFormat',
inject: ['openmct', 'objectPath'],
inject: ['openmct'],
data() {
return {
isEditing: this.openmct.editor.isEditing(),

View File

@@ -56,7 +56,6 @@
:index="index"
:multi-select="selectedLayoutItems.length > 1"
:is-editing="isEditing"
@contextClick="updateViewContext"
@move="move"
@endMove="endMove"
@endLineResize="endLineResize"
@@ -141,7 +140,7 @@ function getItemDefinition(itemType, ...options) {
export default {
components: components,
inject: ['openmct', 'objectPath', 'options', 'objectUtils', 'currentView'],
inject: ['openmct', 'options', 'objectPath'],
props: {
domainObject: {
type: Object,
@@ -156,8 +155,7 @@ export default {
return {
initSelectIndex: undefined,
selection: [],
showGrid: true,
viewContext: {}
showGrid: true
};
},
computed: {
@@ -821,12 +819,6 @@ export default {
},
toggleGrid() {
this.showGrid = !this.showGrid;
},
updateViewContext(viewContext) {
this.viewContext.row = viewContext;
},
getViewContext() {
return this.viewContext;
}
}
};

View File

@@ -72,7 +72,7 @@
<script>
import LayoutFrame from './LayoutFrame.vue';
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import { getDefaultNotebook, getNotebookSectionAndPage } from '@/plugins/notebook/utils/notebook-storage.js';
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1];
@@ -102,7 +102,7 @@ export default {
LayoutFrame
},
mixins: [conditionalStylesMixin],
inject: ['openmct', 'objectPath', 'currentView'],
inject: ['openmct', 'objectPath'],
props: {
item: {
type: Object,
@@ -294,6 +294,16 @@ export default {
this.requestHistoricalData(this.domainObject);
}
},
getView() {
return {
getViewContext: () => {
return {
viewHistoricalData: true,
formattedValueForCopy: this.formattedValueForCopy
};
}
};
},
setObject(domainObject) {
this.domainObject = domainObject;
this.mutablePromise = undefined;
@@ -328,41 +338,30 @@ export default {
this.$emit('formatChanged', this.item, format);
},
updateViewContext() {
this.$emit('contextClick', {
viewHistoricalData: true,
formattedValueForCopy: this.formattedValueForCopy
});
},
async getContextMenuActions() {
const defaultNotebook = getDefaultNotebook();
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
const actionsObject = actionCollection.getActionsObject();
let copyToNotebookAction = actionsObject.copyToNotebook;
let defaultNotebookName;
if (defaultNotebook) {
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}`;
}
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`;
} else {
actionsObject.copyToNotebook = undefined;
delete actionsObject.copyToNotebook;
}
return CONTEXT_MENU_ACTIONS
.map(actionKey => {
const action = this.openmct.actions.getAction(actionKey);
if (action.key === 'copyToNotebook') {
action.name = defaultNotebookName;
}
return action;
})
.filter(action => action.name !== undefined);
return CONTEXT_MENU_ACTIONS.map(actionKey => {
return actionsObject[actionKey];
}).filter(action => action !== undefined);
},
async showContextMenu(event) {
this.updateViewContext();
const contextMenuActions = await this.getContextMenuActions();
const menuItems = this.openmct.menus.actionsToMenuItems(contextMenuActions, this.currentObjectPath, this.currentView);
this.openmct.menus.showMenu(event.x, event.y, menuItems);
this.openmct.menus.showMenu(event.x, event.y, contextMenuActions);
},
setStatus(status) {
this.status = status;

View File

@@ -20,81 +20,13 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Layout from './components/DisplayLayout.vue';
import Vue from 'vue';
import objectUtils from 'objectUtils';
import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
import CopyToClipboardAction from './actions/CopyToClipboardAction';
import DisplayLayout from './components/DisplayLayout.vue';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import DisplayLayoutType from './DisplayLayoutType.js';
import objectUtils from 'objectUtils';
import Vue from 'vue';
class DisplayLayoutView {
constructor(openmct, domainObject, objectPath, options) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.options = options;
this.component = undefined;
}
show(container, isEditing) {
this.component = new Vue({
el: container,
components: {
DisplayLayout
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
options: this.options,
objectUtils,
currentView: this
},
data: () => {
return {
domainObject: this.domainObject,
isEditing
};
},
template: '<display-layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></display-layout>'
});
}
getViewContext() {
if (!this.component) {
return {};
}
return this.component.$refs.displayLayout.getViewContext();
}
getSelectionContext() {
return {
item: this.domainObject,
supportsMultiSelect: true,
addElement: this.component && this.component.$refs.displayLayout.addElement,
removeItem: this.component && this.component.$refs.displayLayout.removeItem,
orderItem: this.component && this.component.$refs.displayLayout.orderItem,
duplicateItem: this.component && this.component.$refs.displayLayout.duplicateItem,
switchViewType: this.component && this.component.$refs.displayLayout.switchViewType,
mergeMultipleTelemetryViews: this.component && this.component.$refs.displayLayout.mergeMultipleTelemetryViews,
mergeMultipleOverlayPlots: this.component && this.component.$refs.displayLayout.mergeMultipleOverlayPlots,
toggleGrid: this.component && this.component.$refs.displayLayout.toggleGrid
};
}
onEditModeChange(isEditing) {
this.component.isEditing = isEditing;
}
destroy() {
this.component.$destroy();
this.component = undefined;
}
}
export default function DisplayLayoutPlugin(options) {
return function (openmct) {
@@ -109,7 +41,51 @@ export default function DisplayLayoutPlugin(options) {
return domainObject.type === 'layout';
},
view: function (domainObject, objectPath) {
return new DisplayLayoutView(openmct, domainObject, objectPath, options);
let component;
return {
show(container) {
component = new Vue({
el: container,
components: {
Layout
},
provide: {
openmct,
objectUtils,
options,
objectPath
},
data() {
return {
domainObject: domainObject,
isEditing: openmct.editor.isEditing()
};
},
template: '<layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></layout>'
});
},
getSelectionContext() {
return {
item: domainObject,
supportsMultiSelect: true,
addElement: component && component.$refs.displayLayout.addElement,
removeItem: component && component.$refs.displayLayout.removeItem,
orderItem: component && component.$refs.displayLayout.orderItem,
duplicateItem: component && component.$refs.displayLayout.duplicateItem,
switchViewType: component && component.$refs.displayLayout.switchViewType,
mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews,
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots,
toggleGrid: component && component.$refs.displayLayout.toggleGrid
};
},
onEditModeChange: function (isEditing) {
component.isEditing = isEditing;
},
destroy() {
component.$destroy();
}
};
},
priority() {
return 100;

View File

@@ -1,51 +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.
*****************************************************************************/
<template>
<a class="c-hyperlink"
:class="{
'c-hyperlink--button' : isButton
}"
:target="domainObject.linkTarget"
:href="domainObject.url"
>
<span class="c-hyperlink__label">{{ domainObject.displayText }}</span>
</a>
</template>
<script>
export default {
inject: ['domainObject'],
computed: {
isButton() {
if (this.domainObject.displayFormat === "link") {
return false;
}
return true;
}
}
};
</script>

View File

@@ -1,89 +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 HyperlinkProvider from './HyperlinkProvider';
export default function () {
return function install(openmct) {
openmct.types.addType('hyperlink', {
name: 'Hyperlink',
key: 'hyperlink',
description: 'A hyperlink to redirect to a different link',
creatable: true,
cssClass: 'icon-chain-links',
initialize: function (domainObject) {
domainObject.displayFormat = "link";
domainObject.linkTarget = "_self";
},
form: [
{
"key": "url",
"name": "URL",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayText",
"name": "Text to Display",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayFormat",
"name": "Display Format",
"control": "select",
"options": [
{
"name": "Link",
"value": "link"
},
{
"name": "Button",
"value": "button"
}
],
"cssClass": "l-inline"
},
{
"key": "linkTarget",
"name": "Tab to Open Hyperlink",
"control": "select",
"options": [
{
"name": "Open in this tab",
"value": "_self"
},
{
"name": "Open in a new tab",
"value": "_blank"
}
],
"cssClass": "l-inline"
}
]
});
openmct.objectViews.addProvider(new HyperlinkProvider(openmct));
};
}

View File

@@ -1,130 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from "utils/testing";
import HyperlinkPlugin from "./plugin";
function getView(openmct, domainObj, objectPath) {
const applicableViews = openmct.objectViews.get(domainObj, objectPath);
const hyperLinkView = applicableViews.find((viewProvider) => viewProvider.key === 'hyperlink.view');
return hyperLinkView.view(domainObj);
}
function destroyView(view) {
return view.destroy();
}
describe("The controller for hyperlinks", function () {
let mockDomainObject;
let mockObjectPath;
let openmct;
let element;
let child;
let view;
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock hyperlink',
type: 'hyperlink',
identifier: {
key: 'mock-hyperlink',
namespace: ''
}
}
];
mockDomainObject = {
displayFormat: "",
linkTarget: "",
name: "Unnamed HyperLink",
type: "hyperlink",
location: "f69c21ac-24ef-450c-8e2f-3d527087d285",
modified: 1627483839783,
url: "123",
displayText: "123",
persisted: 1627483839783,
id: "3d9c243d-dffb-446b-8474-d9931a99d679",
identifier: {
namespace: "",
key: "3d9c243d-dffb-446b-8474-d9931a99d679"
}
};
openmct = createOpenMct();
openmct.install(new HyperlinkPlugin());
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
element.appendChild(child);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
destroyView(view);
return resetApplicationState(openmct);
});
it("knows when it should open a new tab", () => {
mockDomainObject.displayFormat = "link";
mockDomainObject.linkTarget = "_blank";
view = getView(openmct, mockDomainObject, mockObjectPath);
view.show(child, true);
expect(element.querySelector('.c-hyperlink').target).toBe('_blank');
});
it("knows when it should open in the same tab", function () {
mockDomainObject.displayFormat = "button";
mockDomainObject.linkTarget = "_self";
view = getView(openmct, mockDomainObject, mockObjectPath);
view.show(child, true);
expect(element.querySelector('.c-hyperlink').target).toBe('_self');
});
it("knows when it is a button", function () {
mockDomainObject.displayFormat = "button";
view = getView(openmct, mockDomainObject, mockObjectPath);
view.show(child, true);
expect(element.querySelector('.c-hyperlink--button')).toBeDefined();
});
it("knows when it is a link", function () {
mockDomainObject.displayFormat = "link";
view = getView(openmct, mockDomainObject, mockObjectPath);
view.show(child, true);
expect(element.querySelector('.c-hyperlink')).not.toHaveClass('c-hyperlink--button');
});
});

View File

@@ -1,37 +0,0 @@
import ImageryViewLayout from './components/ImageryViewLayout.vue';
import Vue from 'vue';
export default class ImageryView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
components: {
ImageryViewLayout
},
provide: {
openmct: this.openmct,
domainObject: this.domainObject,
objectPath: this.objectPath,
currentView: this
},
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
});
}
destroy() {
this.component.$destroy();
this.component = undefined;
}
_getInstance() {
return this.component;
}
}

View File

@@ -19,7 +19,9 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageryView from './ImageryView';
import ImageryViewLayout from './components/ImageryViewLayout.vue';
import Vue from 'vue';
export default function ImageryViewProvider(openmct) {
const type = 'example.imagery';
@@ -40,8 +42,31 @@ export default function ImageryViewProvider(openmct) {
canView: function (domainObject) {
return hasImageTelemetry(domainObject);
},
view: function (domainObject, objectPath) {
return new ImageryView(openmct, domainObject, objectPath);
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
ImageryViewLayout
},
provide: {
openmct,
domainObject
},
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
},
_getInstance: function () {
return component;
}
};
}
};
}

View File

@@ -58,7 +58,6 @@
<div ref="imageBG"
class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }"
@click="expand"
>
<div class="image-wrapper"
:style="{
@@ -171,9 +170,8 @@
<script>
import _ from 'lodash';
import moment from 'moment';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
const DEFAULT_DURATION_FORMATTER = 'duration';
const REFRESH_CSS_MS = 500;
@@ -197,7 +195,7 @@ export default {
components: {
Compass
},
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
inject: ['openmct', 'domainObject'],
data() {
let timeSystem = this.openmct.time.timeSystem();
@@ -470,16 +468,6 @@ export default {
}
},
methods: {
expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions();
const viewLargeAction = visibleActions
&& visibleActions.find(action => action.key === 'large.view');
if (viewLargeAction && viewLargeAction.appliesTo(this.objectPath, this.currentView)) {
viewLargeAction.onItemClicked();
}
},
async initializeRelatedTelemetry() {
this.relatedTelemetry = new RelatedTelemetry(
this.openmct,

View File

@@ -1,7 +1,7 @@
.c-imagery {
display: flex;
flex-direction: column;
height: 100%;
flex: 1 1 auto;
overflow: hidden;
&:focus {

View File

@@ -280,7 +280,7 @@ describe("The Imagery View Layout", () => {
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
xit("should show the clicked thumbnail as the main image", (done) => {
it("should show the clicked thumbnail as the main image", (done) => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
@@ -317,7 +317,7 @@ describe("The Imagery View Layout", () => {
});
});
xit("should navigate via arrow keys", (done) => {
it("should navigate via arrow keys", (done) => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',

View File

@@ -1,4 +1,4 @@
import { getDefaultNotebook, getNotebookSectionAndPage } from '../utils/notebook-storage';
import { getDefaultNotebook } from '../utils/notebook-storage';
import { addNotebookEntry } from '../utils/notebook-entries';
export default class CopyToNotebookAction {
@@ -15,35 +15,26 @@ export default class CopyToNotebookAction {
copyToNotebook(entryText) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.identifier)
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText);
const { section, page } = getNotebookSectionAndPage(domainObject, notebookStorage.defaultSectionId, notebookStorage.defaultPageId);
if (!section || !page) {
return;
}
const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`;
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this.openmct.notifications.info(msg);
});
}
invoke(objectPath, view) {
const formattedValueForCopy = view.getViewContext().row.formattedValueForCopy;
invoke(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
this.copyToNotebook(formattedValueForCopy());
this.copyToNotebook(viewContext.formattedValueForCopy());
}
appliesTo(objectPath, view = {}) {
const viewContext = view.getViewContext && view.getViewContext();
const row = viewContext && viewContext.row;
if (!row) {
return;
}
let viewContext = view.getViewContext && view.getViewContext();
return row.formattedValueForCopy
&& typeof row.formattedValueForCopy === 'function';
return viewContext && viewContext.formattedValueForCopy
&& typeof viewContext.formattedValueForCopy === 'function';
}
}

View File

@@ -43,16 +43,14 @@
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="getSelectedPageId()"
:selected-page-id="selectedPageId"
:default-section-id="defaultSectionId"
:selected-section-id="getSelectedSectionId()"
:selected-section-id="selectedSectionId"
: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"
@@ -138,7 +136,7 @@ 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, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
import objectUtils from 'objectUtils';
@@ -166,10 +164,8 @@ export default {
},
data() {
return {
defaultPageId: this.getDefaultPageId(),
defaultSectionId: this.getDefaultSectionId(),
selectedSectionId: this.getSelectedSectionId(),
selectedPageId: this.getSelectedPageId(),
selectedSectionId: this.getDefaultSectionId(),
selectedPageId: this.getDefaultPageId(),
defaultSort: this.domainObject.configuration.defaultSort,
focusEntryId: null,
search: '',
@@ -180,6 +176,12 @@ 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) || [];
@@ -201,38 +203,24 @@ export default {
},
selectedPage() {
const pages = this.getPages();
if (!pages.length) {
return undefined;
}
const selectedPage = pages.find(page => page.id === this.selectedPageId);
if (selectedPage) {
return selectedPage;
}
const defaultPage = pages.find(page => page.id === this.defaultPageId);
if (defaultPage) {
return defaultPage;
}
return this.pages[0];
},
selectedSection() {
if (!this.sections.length) {
if (!selectedPage && !pages.length) {
return undefined;
}
const selectedSection = this.sections.find(section => section.id === this.selectedSectionId);
if (selectedSection) {
return selectedSection;
return pages[0];
},
selectedSection() {
if (!this.sections.length) {
return null;
}
const defaultSection = this.sections.find(section => section.id === this.defaultSectionId);
if (defaultSection) {
return defaultSection;
}
return this.sections[0];
return this.sections.find(section => section.id === this.selectedSectionId);
}
},
watch: {
@@ -313,29 +301,26 @@ 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.getDefaultSectionId() || this.getSelectedSectionId();
let pageId = this.getPageIdFromUrl() || this.getDefaultPageId() || this.getSelectedPageId();
let sectionId = this.getSectionIdFromUrl() || this.selectedSectionId;
let pageId = this.getPageIdFromUrl() || this.selectedPageId;
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 {
name: this.domainObject.name,
identifier: this.domainObject.identifier,
link: this.getLinktoNotebook(),
defaultSectionId: section.id,
defaultPageId: page.id
notebookMeta,
page,
section
};
},
deleteEntry(entryId) {
@@ -434,21 +419,35 @@ export default {
this.sidebarCoversEntries = sidebarCoversEntries;
},
getDefaultPageId() {
return this.isDefaultNotebook()
? getDefaultNotebook().defaultPageId
: undefined;
let defaultPageId;
if (this.isDefaultNotebook()) {
defaultPageId = getDefaultNotebook().page.id;
} else {
const firstSection = this.getSections()[0];
defaultPageId = firstSection && firstSection.pages[0].id;
}
return defaultPageId;
},
isDefaultNotebook() {
const defaultNotebook = getDefaultNotebook();
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.identifier;
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.notebookMeta.identifier;
return defaultNotebookIdentifier !== null
&& this.openmct.objects.areIdsEqual(defaultNotebookIdentifier, this.domainObject.identifier);
},
getDefaultSectionId() {
return this.isDefaultNotebook()
? getDefaultNotebook().defaultSectionId
: undefined;
let defaultSectionId;
if (this.isDefaultNotebook()) {
defaultSectionId = getDefaultNotebook().section.id;
} else {
const firstSection = this.getSections()[0];
defaultSectionId = firstSection && firstSection.id;
}
return defaultSectionId;
},
getDefaultNotebookObject() {
const oldNotebookStorage = getDefaultNotebook();
@@ -456,7 +455,7 @@ export default {
return null;
}
return this.openmct.objects.get(oldNotebookStorage.identifier);
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier);
},
getLinktoNotebook() {
const objectPath = this.openmct.router.path;
@@ -574,22 +573,6 @@ 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();
@@ -633,26 +616,51 @@ export default {
},
async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
const isSameNotebook = defaultNotebookObject
&& objectUtils.makeKeyString(defaultNotebookObject.identifier) === objectUtils.makeKeyString(notebookStorage.identifier);
if (!isSameNotebook) {
if (!defaultNotebookObject) {
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
this.removeDefaultClass(defaultNotebookObject);
}
if (!defaultNotebookObject || !isSameNotebook) {
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
}
if (this.defaultSectionId !== notebookStorage.defaultSectionId) {
setDefaultNotebookSectionId(notebookStorage.defaultSectionId);
this.defaultSectionId = notebookStorage.defaultSectionId;
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
this.defaultSectionId = notebookStorage.section.id;
setDefaultNotebookSection(notebookStorage.section);
}
if (this.defaultPageId !== notebookStorage.defaultPageId) {
setDefaultNotebookPageId(notebookStorage.defaultPageId);
this.defaultPageId = notebookStorage.defaultPageId;
if (this.defaultPageId && this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
this.defaultPageId = notebookStorage.page.id;
setDefaultNotebookPage(notebookStorage.page);
}
},
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;
@@ -660,26 +668,26 @@ export default {
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.identifier.key !== this.domainObject.identifier.key) {
|| notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
return;
}
const defaultNotebookSectionId = notebookStorage.defaultSectionId;
if (defaultNotebookSectionId === id) {
const section = sections.find(s => s.id === id);
if (!section) {
this.removeDefaultClass(this.domainObject);
clearDefaultNotebook();
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();
return;
}
}
if (id !== defaultNotebookSectionId) {
return;
}
setDefaultNotebookSectionId(defaultNotebookSectionId);
if (id !== defaultNotebookSection.id) {
return;
}
setDefaultNotebookSection(section);
},
updateEntry(entry) {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
@@ -707,27 +715,19 @@ export default {
sectionId: this.selectedSectionId
});
},
sectionsChanged({ sections, id = undefined }) {
sectionsChanged({ sections, id = null }) {
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 pageId = this.selectedSection.pages[0].id;
this.selectPage(pageId);
const defaultPageId = this.selectedSection.pages[0].id;
this.selectPage(defaultPageId);
this.syncUrlWithPageAndSection();
}

View File

@@ -31,7 +31,6 @@ import PainterroInstance from '../utils/painterroInstance';
import SnapshotTemplate from './snapshot-template.html';
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
import ImageExporter from '../../../exporters/ImageExporter';
import PopupMenu from './PopupMenu.vue';
import Vue from 'vue';
@@ -72,7 +71,7 @@ export default {
},
mounted() {
this.addPopupMenuItems();
this.imageExporter = new ImageExporter(this.openmct);
this.exportImageService = this.openmct.$injector.get('exportImageService');
},
methods: {
addPopupMenuItems() {
@@ -102,6 +101,7 @@ export default {
buttons: [
{
label: 'Cancel',
emphasis: true,
callback: () => {
painterroInstance.dismiss();
annotateOverlay.dismiss();
@@ -109,7 +109,6 @@ export default {
},
{
label: 'Save',
emphasis: true,
callback: () => {
painterroInstance.save((snapshotObject) => {
annotateOverlay.dismiss();
@@ -235,9 +234,9 @@ export default {
let element = this.snapshot.$refs['snapshot-image'];
if (type === 'png') {
this.imageExporter.exportPNG(element, this.embed.name);
this.exportImageService.exportPNG(element, this.embed.name);
} else {
this.imageExporter.exportJPG(element, this.embed.name);
this.exportImageService.exportJPG(element, this.embed.name);
}
},
previewEmbed() {

View File

@@ -17,7 +17,7 @@
<script>
import Snapshot from '../snapshot';
import { getDefaultNotebook, getNotebookSectionAndPage, validateNotebookStorageObject } from '../utils/notebook-storage';
import { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage';
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
export default {
@@ -56,10 +56,11 @@ export default {
this.setDefaultNotebookStatus();
},
methods: {
getDefaultNotebookObject() {
async getDefaultNotebookObject() {
const defaultNotebook = getDefaultNotebook();
const defaultNotebookObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
return defaultNotebook && this.openmct.objects.get(defaultNotebook.identifier);
return defaultNotebookObject;
},
async showMenu(event) {
const notebookTypes = [];
@@ -69,28 +70,26 @@ export default {
const defaultNotebookObject = await this.getDefaultNotebookObject();
if (defaultNotebookObject) {
const defaultNotebook = getDefaultNotebook();
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}`;
const name = defaultNotebookObject.name;
notebookTypes.push({
cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`,
onItemClicked: () => {
return this.snapshot(NOTEBOOK_DEFAULT);
}
});
}
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}`,
callBack: () => {
return this.snapshot(NOTEBOOK_DEFAULT);
}
});
}
notebookTypes.push({
cssClass: 'icon-camera',
name: 'Save to Notebook Snapshots',
onItemClicked: () => {
callBack: () => {
return this.snapshot(NOTEBOOK_SNAPSHOT);
}
});
@@ -120,8 +119,9 @@ export default {
},
setDefaultNotebookStatus() {
let defaultNotebookObject = getDefaultNotebook();
if (defaultNotebookObject) {
let notebookIdentifier = defaultNotebookObject.identifier;
if (defaultNotebookObject && defaultNotebookObject.notebookMeta) {
let notebookIdentifier = defaultNotebookObject.notebookMeta.identifier;
this.openmct.status.set(notebookIdentifier, 'notebook-default');
}

View File

@@ -7,10 +7,10 @@
<div class="c-object-label__type-icon icon-camera"></div>
<div class="c-object-label__name">
Notebook Snapshots
</div>
<div v-if="snapshots.length"
class="l-browse-bar__object-details"
>{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
<span v-if="snapshots.length"
class="l-browse-bar__object-details"
>&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
</span>
</div>
</div>
<PopupMenu v-if="snapshots.length > 0"

View File

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

View File

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

View File

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

View File

@@ -4,10 +4,8 @@
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w">
<span class="c-object-label l-browse-bar__object-name"
v-bind:class="cssClass"
>
<span class="c-object-label__type-icon"
v-bind:class="cssClass"
></span>
<span class="c-object-label__name">{{ name }}</span>
</span>
</div>

View File

@@ -1,27 +1,27 @@
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
import { getDefaultNotebook, getNotebookSectionAndPage, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
import { getDefaultNotebook, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
import SnapshotContainer from './snapshot-container';
import ImageExporter from '../../exporters/ImageExporter';
export default class Snapshot {
constructor(openmct) {
this.openmct = openmct;
this.snapshotContainer = new SnapshotContainer(openmct);
this.imageExporter = new ImageExporter(openmct);
this.capture = this.capture.bind(this);
this._saveSnapShot = this._saveSnapShot.bind(this);
}
capture(snapshotMeta, notebookType, domElement) {
const exportImageService = this.openmct.$injector.get('exportImageService');
const options = {
className: 's-status-taking-snapshot',
thumbnailSize: DEFAULT_SIZE
};
this.imageExporter.exportPNGtoSRC(domElement, options)
exportImageService.exportPNGtoSRC(domElement, options)
.then(function ({blob, thumbnail}) {
const reader = new window.FileReader();
reader.readAsDataURL(blob);
@@ -58,25 +58,20 @@ export default class Snapshot {
*/
_saveToDefaultNoteBook(embed) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.identifier)
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
.then(async (domainObject) => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
let link = notebookStorage.link;
let link = notebookStorage.notebookMeta.link;
// Backwards compatibility fix (old notebook model without link)
if (!link) {
link = await getDefaultNotebookLink(this.openmct, domainObject);
notebookStorage.link = link;
notebookStorage.notebookMeta.link = link;
setDefaultNotebook(this.openmct, notebookStorage);
}
const { section, page } = getNotebookSectionAndPage(domainObject, notebookStorage.defaultSectionId, notebookStorage.defaultPageId);
if (!section || !page) {
return;
}
const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`;
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this._showNotification(msg, link);
});

View File

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

View File

@@ -23,13 +23,28 @@ import * as NotebookEntries from './notebook-entries';
import { createOpenMct, resetApplicationState } from 'utils/testing';
const notebookStorage = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
notebookMeta: {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
}
},
defaultSectionId: '03a79b6a-971c-4e56-9892-ec536332c3f0',
defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00'
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'
}
};
const notebookEntries = {

View File

@@ -19,22 +19,18 @@ function defaultNotebookObjectChanged(newDomainObject) {
clearDefaultNotebook();
}
function observeDefaultNotebookObject(openmct, notebookStorage, domainObject) {
function observeDefaultNotebookObject(openmct, notebookMeta, domainObject) {
if (currentNotebookObjectIdentifier
&& objectUtils.makeKeyString(currentNotebookObjectIdentifier) === objectUtils.makeKeyString(notebookStorage.identifier)) {
&& objectUtils.makeKeyString(currentNotebookObjectIdentifier) === objectUtils.makeKeyString(notebookMeta.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) {
@@ -43,8 +39,6 @@ function saveDefaultNotebook(notebookStorage) {
export function clearDefaultNotebook() {
currentNotebookObjectIdentifier = null;
removeListener();
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, null);
}
@@ -54,17 +48,6 @@ 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;
@@ -76,9 +59,9 @@ export async function getDefaultNotebookLink(openmct, domainObject = null) {
.reverse()
.join('/')
);
const { defaultPageId, defaultSectionId } = getDefaultNotebook();
const { page, section } = getDefaultNotebook();
return `#/browse/${path}?sectionId=${defaultSectionId}&pageId=${defaultPageId}`;
return `#/browse/${path}?sectionId=${section.id}&pageId=${page.id}`;
}
export function setDefaultNotebook(openmct, notebookStorage, domainObject) {
@@ -86,15 +69,15 @@ export function setDefaultNotebook(openmct, notebookStorage, domainObject) {
saveDefaultNotebook(notebookStorage);
}
export function setDefaultNotebookSectionId(sectionId) {
export function setDefaultNotebookSection(section) {
const notebookStorage = getDefaultNotebook();
notebookStorage.defaultSectionId = sectionId;
notebookStorage.section = section;
saveDefaultNotebook(notebookStorage);
}
export function setDefaultNotebookPageId(pageId) {
export function setDefaultNotebookPage(page) {
const notebookStorage = getDefaultNotebook();
notebookStorage.defaultPageId = pageId;
notebookStorage.page = page;
saveDefaultNotebook(notebookStorage);
}

View File

@@ -23,44 +23,37 @@
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 = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
notebookMeta: {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
}
},
defaultSectionId: 'temp-section',
defaultPageId: 'temp-page'
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'
}
};
let openmct;
@@ -111,7 +104,7 @@ describe('Notebook Storage:', () => {
expect(JSON.stringify(defaultNotebook)).toBe(JSON.stringify(notebookStorage));
});
it('has correct section on setDefaultNotebookSectionId', () => {
it('has correct section on setDefaultNotebookSection', () => {
const section = {
id: 'new-temp-section',
isDefault: true,
@@ -122,14 +115,14 @@ describe('Notebook Storage:', () => {
};
NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject);
NotebookStorage.setDefaultNotebookSectionId(section.id);
NotebookStorage.setDefaultNotebookSection(section);
const defaultNotebook = NotebookStorage.getDefaultNotebook();
const defaultSectionId = defaultNotebook.defaultSectionId;
expect(section.id).toBe(defaultSectionId);
const newSection = defaultNotebook.section;
expect(JSON.stringify(section)).toBe(JSON.stringify(newSection));
});
it('has correct page on setDefaultNotebookPageId', () => {
it('has correct page on setDefaultNotebookPage', () => {
const page = {
id: 'new-temp-page',
isDefault: true,
@@ -139,52 +132,10 @@ describe('Notebook Storage:', () => {
};
NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject);
NotebookStorage.setDefaultNotebookPageId(page.id);
NotebookStorage.setDefaultNotebookPage(page);
const defaultNotebook = NotebookStorage.getDefaultNotebook();
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');
});
const newPage = defaultNotebook.page;
expect(JSON.stringify(page)).toBe(JSON.stringify(newPage));
});
});

View File

@@ -1,106 +0,0 @@
(function () {
const connections = [];
let connected = false;
const controller = new AbortController();
const signal = controller.signal;
self.onconnect = function (e) {
let port = e.ports[0];
connections.push(port);
port.postMessage({
type: 'connection',
connectionId: connections.length
});
port.onmessage = async function (event) {
if (event.data.request === 'close') {
connections.splice(event.data.connectionId - 1, 1);
if (connections.length <= 0) {
// abort any outstanding requests if there's nobody listening to it.
controller.abort();
}
return;
}
if (event.data.request === 'changes') {
if (connected === true) {
return;
}
connected = true;
let url = event.data.url;
let body = event.data.body;
let error = false;
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
// style=main_only returns only the current winning revision of the document
const response = await fetch(url, {
method: 'POST',
headers: {
"Content-Type": 'application/json'
},
signal,
body
});
let reader;
if (response.body === undefined) {
error = true;
} else {
reader = response.body.getReader();
}
while (!error) {
const {done, value} = await reader.read();
//done is true when we lose connection with the provider
if (done) {
error = true;
}
if (value) {
let chunk = new Uint8Array(value.length);
chunk.set(value, 0);
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
decodedChunk.forEach((doc, index) => {
try {
if (doc) {
const objectChanges = JSON.parse(doc);
connections.forEach(function (connection) {
connection.postMessage({
objectChanges
});
});
}
} catch (decodeError) {
//do nothing;
console.log(decodeError);
}
});
}
}
}
if (error) {
port.postMessage({
error
});
}
}
};
port.start();
};
self.onerror = function () {
//do nothing
console.log('Error on feed');
};
}());

View File

@@ -40,65 +40,6 @@ export default class CouchObjectProvider {
this.batchIds = [];
}
/**
* @private
*/
startSharedWorker() {
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);
sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this);
sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this);
sharedWorker.port.start();
this.openmct.on('destroy', () => {
this.changesFeedSharedWorker.port.postMessage({
request: 'close',
connectionId: this.changesFeedSharedWorkerConnectionId
});
this.changesFeedSharedWorker.port.close();
});
return sharedWorker;
}
onSharedWorkerMessageError(event) {
console.log('Error', event);
}
onSharedWorkerMessage(event) {
if (event.data.type === 'connection') {
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
} else {
const error = event.data.error;
if (error && Object.keys(this.observers).length > 0) {
this.observeObjectChanges();
return;
}
let objectChanges = event.data.objectChanges;
objectChanges.identifier = {
namespace: this.namespace,
key: objectChanges.id
};
let keyString = this.openmct.objects.makeKeyString(objectChanges.identifier);
//TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have.
let observersForObject = this.observers[keyString];
if (observersForObject) {
observersForObject.forEach(async (observer) => {
const updatedObject = await this.get(objectChanges.identifier);
observer(updatedObject);
});
}
}
}
//backwards compatibility, options used to be a url. Now it's an object
_normalize(options) {
if (typeof options === 'string') {
@@ -379,7 +320,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 && this.isObservingObjectChanges()) {
if (Object.keys(this.observers).length === 0) {
this.stopObservingObjectChanges();
}
}
@@ -393,8 +334,9 @@ export default class CouchObjectProvider {
/**
* @private
*/
observeObjectChanges() {
async observeObjectChanges() {
const controller = new AbortController();
const signal = controller.signal;
let filter = {selector: {}};
if (this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.length > 1) {
@@ -412,6 +354,17 @@ export default class CouchObjectProvider {
};
}
let error = false;
if (typeof this.stopObservingObjectChanges === 'function') {
this.stopObservingObjectChanges();
}
this.stopObservingObjectChanges = () => {
controller.abort();
delete this.stopObservingObjectChanges;
};
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
// style=main_only returns only the current winning revision of the document
let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`;
@@ -422,52 +375,6 @@ export default class CouchObjectProvider {
body = JSON.stringify(filter);
}
if (typeof SharedWorker === 'undefined') {
this.fetchChanges(url, body);
} else {
this.initiateSharedWorkerFetchChanges(url, body);
}
}
/**
* @private
*/
initiateSharedWorkerFetchChanges(url, body) {
if (!this.changesFeedSharedWorker) {
this.changesFeedSharedWorker = this.startSharedWorker();
if (this.isObservingObjectChanges()) {
this.stopObservingObjectChanges();
}
this.stopObservingObjectChanges = () => {
delete this.stopObservingObjectChanges;
};
this.changesFeedSharedWorker.port.postMessage({
request: 'changes',
body,
url
});
}
}
async fetchChanges(url, body) {
const controller = new AbortController();
const signal = controller.signal;
let error = false;
if (this.isObservingObjectChanges()) {
this.stopObservingObjectChanges();
}
this.stopObservingObjectChanges = () => {
controller.abort();
delete this.stopObservingObjectChanges;
};
const response = await fetch(url, {
method: 'POST',
signal,

View File

@@ -67,7 +67,7 @@ export default {
TimelineAxis,
SwimLane
},
inject: ['openmct', 'domainObject', 'path'],
inject: ['openmct', 'domainObject'],
props: {
options: {
type: Object,
@@ -99,37 +99,21 @@ export default {
this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
this.setTimeContext();
this.updateViewBounds();
this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities);
this.openmct.time.on("bounds", this.updateViewBounds);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
beforeDestroy() {
clearInterval(this.resizeTimer);
this.stopFollowingTimeContext();
this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
this.openmct.time.off("bounds", this.updateViewBounds);
if (this.unlisten) {
this.unlisten();
}
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.timeContext.on("timeContext", this.setTimeContext);
this.followTimeContext();
},
followTimeContext() {
this.updateViewBounds(this.timeContext.bounds());
this.timeContext.on("timeSystem", this.setScaleAndPlotActivities);
this.timeContext.on("bounds", this.updateViewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.setScaleAndPlotActivities);
this.timeContext.off("bounds", this.updateViewBounds);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
observeForChanges(mutatedObject) {
this.getPlanData(mutatedObject);
this.setScaleAndPlotActivities();
@@ -157,11 +141,8 @@ export default {
getPlanData(domainObject) {
this.planData = getValidatedPlan(domainObject);
},
updateViewBounds(bounds) {
if (bounds) {
this.viewBounds = Object.assign({}, bounds);
}
updateViewBounds() {
this.viewBounds = this.openmct.time.bounds();
//Add a 50% padding to the end bounds to look ahead
let timespan = (this.viewBounds.end - this.viewBounds.start);
let padding = timespan / 2;

View File

@@ -54,8 +54,7 @@ export default function PlanViewProvider(openmct) {
},
provide: {
openmct,
domainObject,
path: objectPath
domainObject
},
data() {
return {

View File

@@ -49,6 +49,10 @@ describe('the plugin', function () {
child.style.height = '480px';
element.appendChild(child);
openmct.time.timeSystem('utc', {
start: 1597160002854,
end: 1597181232854
});
openmct.on('start', done);
openmct.start(appHolder);
});
@@ -101,11 +105,6 @@ describe('the plugin', function () {
let planView;
beforeEach(() => {
openmct.time.timeSystem('utc', {
start: 1597160002854,
end: 1597181232854
});
planDomainObject = {
identifier: {
key: 'test-object',

View File

@@ -173,7 +173,7 @@ export default {
MctTicks,
MctChart
},
inject: ['openmct', 'domainObject', 'path'],
inject: ['openmct', 'domainObject'],
props: {
options: {
type: Object,
@@ -249,9 +249,6 @@ export default {
},
mounted() {
eventHelpers.extend(this);
this.updateRealTime = this.updateRealTime.bind(this);
this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
this.setTimeContext = this.setTimeContext.bind(this);
this.config = this.getConfig();
@@ -268,7 +265,7 @@ export default {
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
this.openmct.objectViews.on('clearData', this.clearData);
this.setTimeContext();
this.followTimeConductor();
this.loaded = true;
@@ -281,27 +278,11 @@ export default {
this.destroy();
},
methods: {
setTimeContext(updatedKey) {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.timeContext.on('timeContext', this.setTimeContext);
this.followTimeContext();
},
followTimeContext() {
this.updateDisplayBounds(this.timeContext.bounds());
this.timeContext.on('clock', this.updateRealTime);
this.timeContext.on('bounds', this.updateDisplayBounds);
followTimeConductor() {
this.openmct.time.on('clock', this.updateRealTime);
this.openmct.time.on('bounds', this.updateDisplayBounds);
this.synchronized(true);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("clock", this.updateRealTime);
this.timeContext.off("bounds", this.updateDisplayBounds);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
@@ -446,12 +427,9 @@ export default {
this.skipReloadOnInteraction = false;
this.loadMoreData(newRange, true);
} else {
// If we're not panning or zooming (time conductor and plot x-axis times are not out of sync)
// Drop any data that is more than 1x (max-min) before min.
// Limit these purges to once a second.
const isPanningOrZooming = this.isTimeOutOfSync;
const purgeRecords = !isPanningOrZooming && (!this.nextPurge || (this.nextPurge < Date.now()));
if (purgeRecords) {
if (!this.nextPurge || this.nextPurge < Date.now()) {
const keepRange = {
min: newRange.min - (newRange.max - newRange.min),
max: newRange.max
@@ -486,7 +464,7 @@ export default {
* displays can update accordingly.
*/
synchronized(value) {
const isLocalClock = this.timeContext.clock();
const isLocalClock = this.openmct.time.clock();
if (typeof value !== 'undefined') {
this._synchronized = value;
@@ -955,7 +933,7 @@ export default {
},
setYAxisKey(yKey) {
this.config.series.models[0].set('yKey', yKey);
this.config.series.models[0].emit('change:yKey', yKey);
},
pause() {
@@ -967,7 +945,7 @@ export default {
},
showSynchronizeDialog() {
const isLocalClock = this.timeContext.clock();
const isLocalClock = this.openmct.time.clock();
if (isLocalClock !== undefined) {
const message = `
This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds.
@@ -1002,9 +980,9 @@ export default {
},
synchronizeTimeConductor() {
this.timeContext.stopClock();
this.openmct.time.stopClock();
const range = this.config.xAxis.get('displayRange');
this.timeContext.bounds({
this.openmct.time.bounds({
start: range.min,
end: range.max
});
@@ -1015,7 +993,6 @@ export default {
configStore.deleteStore(this.config.id);
this.stopListening();
if (this.checkForSize) {
clearInterval(this.checkForSize);
delete this.checkForSize;
@@ -1031,7 +1008,8 @@ export default {
this.plotContainerResizeObserver.disconnect();
this.stopFollowingTimeContext();
this.openmct.time.off('clock', this.updateRealTime);
this.openmct.time.off('bounds', this.updateDisplayBounds);
this.openmct.objectViews.off('clearData', this.clearData);
},
updateStatus(status) {

View File

@@ -72,15 +72,14 @@
</template>
<script>
import eventHelpers from './lib/eventHelpers';
import ImageExporter from '../../exporters/ImageExporter';
import eventHelpers from "./lib/eventHelpers";
import MctPlot from './MctPlot.vue';
export default {
components: {
MctPlot
},
inject: ['openmct', 'domainObject', 'path'],
inject: ['openmct', 'domainObject'],
props: {
options: {
type: Object,
@@ -103,7 +102,8 @@ export default {
},
mounted() {
eventHelpers.extend(this);
this.imageExporter = new ImageExporter(this.openmct);
this.exportImageService = this.openmct.$injector.get('exportImageService');
},
beforeDestroy() {
this.destroy();
@@ -118,12 +118,14 @@ export default {
exportJPG() {
const plotElement = this.$refs.plotContainer;
this.imageExporter.exportJPG(plotElement, 'plot.jpg', 'export-plot');
this.exportImageService.exportJPG(plotElement, 'plot.jpg', 'export-plot');
},
exportPNG() {
const plotElement = this.$refs.plotContainer;
this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot');
this.exportImageService.exportPNG(plotElement, 'plot.png', 'export-plot');
},
toggleCursorGuide() {

View File

@@ -24,23 +24,19 @@ import Plot from './Plot.vue';
import Vue from 'vue';
export default function PlotViewProvider(openmct) {
function hasNumericTelemetry(domainObject) {
function hasTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasDomainAndNumericRange(metadata);
return metadata.values().length > 0 && hasDomainAndRange(metadata);
}
function hasDomainAndNumericRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']);
const domains = metadata.valuesForHints(['domain']);
return domains.length > 0
&& rangeValues.length > 0
&& !rangeValues.every(value => value.format === 'string');
function hasDomainAndRange(metadata) {
return (metadata.valuesForHints(['range']).length > 0
&& metadata.valuesForHints(['domain']).length > 0);
}
function isCompactView(objectPath) {
@@ -48,11 +44,11 @@ export default function PlotViewProvider(openmct) {
}
return {
key: 'plot-single',
key: 'plot-simple',
name: 'Plot',
cssClass: 'icon-telemetry',
canView(domainObject, objectPath) {
return hasNumericTelemetry(domainObject);
return hasTelemetry(domainObject, openmct);
},
view: function (domainObject, objectPath) {
@@ -68,8 +64,7 @@ export default function PlotViewProvider(openmct) {
},
provide: {
openmct,
domainObject,
path: objectPath
domainObject
},
data() {
return {

View File

@@ -53,8 +53,7 @@ export default function OverlayPlotViewProvider(openmct) {
},
provide: {
openmct,
domainObject,
path: objectPath
domainObject
},
data() {
return {

View File

@@ -201,57 +201,15 @@ describe("the plugin", function () {
hints: {
range: 1
}
},
{
key: "yet-another-key",
format: "string",
hints: {
range: 2
}
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple");
expect(plotView).toBeDefined();
});
it("does not provide a plot view if the telemetry is entirely non numeric", () => {
const testTelemetryObject = {
id: "test-object",
type: "test-object",
telemetry: {
values: [{
key: "some-key",
hints: {
domain: 1
}
},
{
key: "other-key",
format: "string",
hints: {
range: 1
}
},
{
key: "yet-another-key",
format: "string",
hints: {
range: 1
}
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
expect(plotView).toBeUndefined();
});
it("provides an overlay plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
@@ -321,10 +279,14 @@ describe("the plugin", function () {
let plotView;
beforeEach(() => {
openmct.time.timeSystem("utc", {
start: 0,
end: 4
});
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
testTelemetryObject = {
identifier: {
namespace: "",
@@ -357,7 +319,7 @@ describe("the plugin", function () {
};
applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple");
plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]);
plotView.show(child, true);
@@ -570,8 +532,7 @@ describe("the plugin", function () {
provide: {
openmct: openmct,
domainObject: stackedPlotObject,
composition: openmct.composition.get(stackedPlotObject),
path: [stackedPlotObject]
composition: openmct.composition.get(stackedPlotObject)
},
template: "<stacked-plot></stacked-plot>"
});
@@ -606,7 +567,7 @@ describe("the plugin", function () {
expect(legend.length).toBe(6);
});
xit("Renders X-axis ticks for the telemetry object", () => {
it("Renders X-axis ticks for the telemetry object", () => {
let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
expect(xAxisElement.length).toBe(1);

View File

@@ -67,15 +67,14 @@
</template>
<script>
import eventHelpers from '../lib/eventHelpers';
import StackedPlotItem from './StackedPlotItem.vue';
import ImageExporter from '../../../exporters/ImageExporter';
import eventHelpers from "../lib/eventHelpers";
import StackedPlotItem from "./StackedPlotItem.vue";
export default {
components: {
StackedPlotItem
},
inject: ['openmct', 'domainObject', 'composition', 'path'],
inject: ['openmct', 'domainObject', 'composition'],
props: {
options: {
type: Object,
@@ -104,7 +103,7 @@ export default {
mounted() {
eventHelpers.extend(this);
this.imageExporter = new ImageExporter(this.openmct);
this.exportImageService = this.openmct.$injector.get('exportImageService');
this.tickWidthMap = {};
@@ -160,9 +159,9 @@ export default {
exportJPG() {
this.hideExportButtons = true;
const plotElement = this.$el;
const plotElement = this.$refs.plotContainer;
this.imageExporter.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot')
this.exportImageService.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot')
.finally(function () {
this.hideExportButtons = false;
}.bind(this));
@@ -171,9 +170,9 @@ export default {
exportPNG() {
this.hideExportButtons = true;
const plotElement = this.$el;
const plotElement = this.$refs.plotContainer;
this.imageExporter.exportPNG(plotElement, 'stacked-plot.png', 'export-plot')
this.exportImageService.exportPNG(plotElement, 'stacked-plot.png', 'export-plot')
.finally(function () {
this.hideExportButtons = false;
}.bind(this));

View File

@@ -28,7 +28,7 @@ import MctPlot from '../MctPlot.vue';
import Vue from "vue";
export default {
inject: ['openmct', 'domainObject', 'path'],
inject: ['openmct', 'domainObject'],
props: {
object: {
type: Object,
@@ -94,7 +94,6 @@ export default {
const openmct = this.openmct;
const object = this.object;
const path = this.path;
const getProps = this.getProps;
let viewContainer = document.createElement('div');
@@ -107,8 +106,7 @@ export default {
},
provide: {
openmct,
domainObject: object,
path
domainObject: object
},
data() {
return {

View File

@@ -55,8 +55,7 @@ export default function StackedPlotViewProvider(openmct) {
provide: {
openmct,
domainObject,
composition: openmct.composition.get(domainObject),
path: objectPath
composition: openmct.composition.get(domainObject)
},
data() {
return {

View File

@@ -23,7 +23,6 @@
define([
'lodash',
'./utcTimeSystem/plugin',
'./remoteClock/plugin',
'./localTimeSystem/plugin',
'./ISOTimeFormat/plugin',
'../../example/generator/plugin',
@@ -63,16 +62,14 @@ define([
'./defaultRootName/plugin',
'./plan/plugin',
'./viewDatumAction/plugin',
'./viewLargeAction/plugin',
'./interceptors/plugin',
'./performanceIndicator/plugin',
'./CouchDBSearchFolder/plugin',
'./timeline/plugin',
'./hyperlink/plugin'
'/node_modules/openmct-mmgis/dist/openmct-mmgis'
], function (
_,
UTCTimeSystem,
RemoteClock,
LocalTimeSystem,
ISOTimeFormat,
GeneratorPlugin,
@@ -112,12 +109,11 @@ define([
DefaultRootName,
PlanLayout,
ViewDatumAction,
ViewLargeAction,
ObjectInterceptors,
PerformanceIndicator,
CouchDBSearchFolder,
Timeline,
Hyperlink
Mmgis
) {
const bundleMap = {
LocalStorage: 'platform/persistence/local',
@@ -135,7 +131,6 @@ define([
plugins.UTCTimeSystem = UTCTimeSystem;
plugins.LocalTimeSystem = LocalTimeSystem;
plugins.RemoteClock = RemoteClock.default;
plugins.ImportExport = ImportExport;
@@ -215,12 +210,11 @@ define([
plugins.DefaultRootName = DefaultRootName.default;
plugins.PlanLayout = PlanLayout.default;
plugins.ViewDatumAction = ViewDatumAction.default;
plugins.ViewLargeAction = ViewLargeAction.default;
plugins.ObjectInterceptors = ObjectInterceptors.default;
plugins.PerformanceIndicator = PerformanceIndicator.default;
plugins.CouchDBSearchFolder = CouchDBSearchFolder.default;
plugins.Timeline = Timeline.default;
plugins.Hyperlink = Hyperlink.default;
plugins.Mmgis = Mmgis.default;
return plugins;
});

View File

@@ -1,132 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DefaultClock from '../../utils/clock/DefaultClock';
/**
* A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the
* application based on a time providing telemetry domainObject.
*
* @param {openmct} Object Instance of OpenMCT
* @param {module:openmct.ObjectAPI~Identifier} identifier An object identifier for
* the time providing telemetry domainObject
* @constructor
*/
export default class RemoteClock extends DefaultClock {
constructor(openmct, identifier) {
super();
this.key = 'remote-clock';
this.openmct = openmct;
this.identifier = identifier;
this.name = 'Remote Clock';
this.description = "Provides telemetry based timestamps from a configurable source.";
this.timeTelemetryObject = undefined;
this.parseTime = undefined;
this.metadata = undefined;
this.lastTick = 0;
this._processDatum = this._processDatum.bind(this);
}
start() {
this.openmct.time.on('timeSystem', this._timeSystemChange);
this.openmct.objects.get(this.identifier).then((domainObject) => {
this.timeTelemetryObject = domainObject;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this._timeSystemChange();
this._requestLatest();
this._subscribe();
}).catch((error) => {
throw new Error(error);
});
}
stop() {
this.openmct.time.off('timeSystem', this._timeSystemChange);
if (this._unsubscribe) {
this._unsubscribe();
}
this.removeAllListeners();
}
/**
* Will start a subscription to the timeTelemetryObject as well
* handle the unsubscribe callback
*
* @private
*/
_subscribe() {
this._unsubscribe = this.openmct.telemetry.subscribe(
this.timeTelemetryObject,
this._processDatum
);
}
/**
* Will request the latest data for the timeTelemetryObject
*
* @private
*/
_requestLatest() {
this.openmct.telemetry.request(this.timeTelemetryObject, {
size: 1,
strategy: 'latest'
}).then(data => {
this._processDatum(data[data.length - 1]);
});
}
/**
* Function to parse the datum from the timeTelemetryObject as well
* as check if it's valid, calls "tick"
*
* @private
*/
_processDatum(datum) {
let time = this.parseTime(datum);
if (time > this.lastTick) {
this.tick(time);
}
}
/**
* Callback function for timeSystem change events
*
* @private
*/
_timeSystemChange() {
let timeSystem = this.openmct.time.timeSystem();
let timeKey = timeSystem.key;
let metadataValue = this.metadata.value(timeKey);
let timeFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
this.parseTime = (datum) => {
return timeFormatter.parse(datum);
};
}
}

View File

@@ -1,152 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
const REMOTE_CLOCK_KEY = 'remote-clock';
const TIME_TELEMETRY_ID = {
namespace: 'remote',
key: 'telemetry'
};
const TIME_VALUE = 12345;
const REQ_OPTIONS = {
size: 1,
strategy: 'latest'
};
const OFFSET_START = -10;
const OFFSET_END = 1;
describe("the RemoteClock plugin", () => {
let openmct;
let object = {
name: 'remote-telemetry',
identifier: TIME_TELEMETRY_ID
};
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe('once installed', () => {
let remoteClock;
let boundsCallback;
let metadataValue = { some: 'value' };
let timeSystem = { key: 'utc' };
let metadata = {
value: () => metadataValue
};
let reqDatum = {
key: TIME_VALUE
};
let formatter = {
parse: (datum) => datum.key
};
beforeEach((done) => {
openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID));
let clocks = openmct.time.getAllClocks();
remoteClock = clocks.filter(clock => clock.key === REMOTE_CLOCK_KEY)[0];
boundsCallback = jasmine.createSpy("boundsCallback");
openmct.time.on('bounds', boundsCallback);
spyOn(remoteClock, '_timeSystemChange').and.callThrough();
spyOn(openmct.telemetry, 'getMetadata').and.returnValue(metadata);
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue(formatter);
spyOn(openmct.telemetry, 'subscribe').and.callThrough();
spyOn(openmct.time, 'on').and.callThrough();
spyOn(openmct.time, 'timeSystem').and.returnValue(timeSystem);
spyOn(metadata, 'value').and.callThrough();
let requestPromiseResolve;
let requestPromise = new Promise((resolve) => {
requestPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
requestPromiseResolve([reqDatum]);
return requestPromise;
});
let objectPromiseResolve;
let objectPromise = new Promise((resolve) => {
objectPromiseResolve = resolve;
});
spyOn(openmct.objects, 'get').and.callFake(() => {
objectPromiseResolve(object);
return objectPromise;
});
openmct.time.clock(REMOTE_CLOCK_KEY, {
start: OFFSET_START,
end: OFFSET_END
});
Promise.all([objectPromiseResolve, requestPromise])
.then(done)
.catch(done);
});
it('is available and sets up initial values and listeners', () => {
expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);
expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);
expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);
expect(remoteClock._timeSystemChange).toHaveBeenCalled();
});
it('will request/store the object based on the identifier passed in', () => {
expect(remoteClock.timeTelemetryObject).toEqual(object);
});
it('will request metadata and set up formatters', () => {
expect(remoteClock.metadata).toEqual(metadata);
expect(metadata.value).toHaveBeenCalled();
expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);
});
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);
});
it('will set up subscriptions correctly', () => {
expect(remoteClock._unsubscribe).toBeDefined();
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum);
});
});
});

View File

@@ -1,32 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import RemoteClock from "./RemoteClock";
/**
* Install a clock that uses a configurable telemetry endpoint.
*/
export default function (identifier) {
return function (openmct) {
openmct.time.addClock(new RemoteClock(openmct, identifier));
};
}

View File

@@ -23,18 +23,20 @@
define([
'EventEmitter',
'lodash',
'./collections/TableRowCollection',
'./TelemetryTableRow',
'./collections/BoundedTableRowCollection',
'./collections/FilteredTableRowCollection',
'./TelemetryTableNameColumn',
'./TelemetryTableRow',
'./TelemetryTableColumn',
'./TelemetryTableUnitColumn',
'./TelemetryTableConfiguration'
], function (
EventEmitter,
_,
TableRowCollection,
TelemetryTableRow,
BoundedTableRowCollection,
FilteredTableRowCollection,
TelemetryTableNameColumn,
TelemetryTableRow,
TelemetryTableColumn,
TelemetryTableUnitColumn,
TelemetryTableConfiguration
@@ -46,23 +48,20 @@ 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);
@@ -103,7 +102,8 @@ define([
}
createTableRowCollections() {
this.tableRows = new TableRowCollection();
this.boundedRows = new BoundedTableRowCollection(this.openmct);
this.filteredRows = new FilteredTableRowCollection(this.boundedRows);
//Fetch any persisted default sort
let sortOptions = this.configuration.getConfiguration().sortOptions;
@@ -113,14 +113,11 @@ define([
key: this.openmct.time.timeSystem().key,
direction: 'asc'
};
this.tableRows.sortBy(sortOptions);
this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);
this.filteredRows.sortBy(sortOptions);
}
loadComposition() {
this.tableComposition = this.openmct.composition.get(this.domainObject);
if (this.tableComposition !== undefined) {
this.tableComposition.load().then((composition) => {
@@ -135,64 +132,66 @@ define([
addTelemetryObject(telemetryObject) {
this.addColumnsForObject(telemetryObject, true);
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
.requestTelemetryCollection(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.requestDataFor(telemetryObject);
this.subscribeTo(telemetryObject);
this.telemetryObjects.push(telemetryObject);
this.emit('object-added', telemetryObject);
}
getTelemetryProcessor(keyString, columnMap, limitEvaluator) {
return (telemetry) => {
//Check that telemetry object has not been removed since telemetry was requested.
if (!this.telemetryObjects[keyString]) {
return;
}
updateFilters(updatedFilters) {
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
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');
}
};
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
this.filters = deepCopiedFilters;
this.clearAndResubscribe();
} else {
this.filters = deepCopiedFilters;
}
}
getTelemetryRemover() {
return (telemetry) => {
if (this.paused) {
this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry));
} else {
this.tableRows.removeRowsByData(telemetry);
}
};
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);
}
/**
@@ -217,72 +216,35 @@ 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.tableRows.outstandingRequests === 0) {
this.tableRows.clear();
this.tableRows.sortBy({
key: this.openmct.time.timeSystem().key,
direction: 'asc'
});
this.tableRows.resubscribe();
if (!isTick && this.outstandingRequests === 0) {
this.filteredRows.clear();
this.boundedRows.clear();
this.boundedRows.sortByTimeSystem(this.openmct.time.timeSystem());
this.telemetryObjects.forEach(this.requestDataFor);
}
}
clearData() {
this.tableRows.clear();
this.filteredRows.clear();
this.boundedRows.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();
@@ -302,18 +264,54 @@ define([
});
}
getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns();
createColumn(metadatum) {
return new TelemetryTableColumn(this.openmct, metadatum);
}
if (columns[objectKeyString]) {
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
createUnitColumn(metadatum) {
return new TelemetryTableUnitColumn(this.openmct, metadatum);
}
return map;
}, {});
}
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 {};
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');
}
buildOptionsFromConfiguration(telemetryObject) {
@@ -325,20 +323,13 @@ define([
return {filters} || {};
}
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');
unsubscribe(keyString) {
this.subscriptions[keyString]();
delete this.subscriptions[keyString];
}
sortBy(sortOptions) {
this.tableRows.sortBy(sortOptions);
this.filteredRows.sortBy(sortOptions);
if (this.openmct.editor.isEditing()) {
let configuration = this.configuration.getConfiguration();
@@ -347,36 +338,21 @@ 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.runDelayedActions();
this.processDatumCache();
this.boundedRows.subscribeToBounds();
}
destroy() {
this.tableRows.destroy();
this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData);
let keystrings = Object.keys(this.telemetryCollections);
keystrings.forEach(this.removeTelemetryCollection);
this.boundedRows.destroy();
this.filteredRows.destroy();
Object.keys(this.subscriptions).forEach(this.unsubscribe, this);
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('timeSystem', this.refreshData);

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