Compare commits
4 Commits
independen
...
mmgis-cust
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2def6dd634 | ||
|
|
16fe1b55c6 | ||
|
|
8037c372b0 | ||
|
|
216a349282 |
@@ -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
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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:
|
||||
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
@@ -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.
|
||||
4
.github/workflows/lighthouse.yml
vendored
4
.github/workflows/lighthouse.yml
vendored
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Vue from 'vue';
|
||||
import Vue from 'Vue';
|
||||
import HelloWorld from './HelloWorld.vue';
|
||||
|
||||
function SimpleVuePlugin() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: {
|
||||
|
||||
21
package.json
21
package.json
@@ -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
|
||||
|
||||
@@ -181,7 +181,7 @@ define([
|
||||
],
|
||||
"category": "contextual",
|
||||
"name": "Stop",
|
||||
"cssClass": "icon-box-round-corners",
|
||||
"cssClass": "icon-box",
|
||||
"priority": "preferred"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -101,7 +101,7 @@ define(
|
||||
name: "Pause"
|
||||
});
|
||||
mockStop.getMetadata.and.returnValue({
|
||||
cssClass: "icon-box-round-corners",
|
||||
cssClass: "icon-box",
|
||||
name: "Stop"
|
||||
});
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
120
platform/features/hyperlink/bundle.js
Normal file
120
platform/features/hyperlink/bundle.js
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
28
platform/features/hyperlink/res/templates/hyperlink.html
Normal file
28
platform/features/hyperlink/res/templates/hyperlink.html
Normal 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>
|
||||
61
platform/features/hyperlink/src/HyperlinkController.js
Normal file
61
platform/features/hyperlink/src/HyperlinkController.js
Normal 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;
|
||||
}
|
||||
|
||||
);
|
||||
89
platform/features/hyperlink/test/HyperlinkControllerSpec.js
Normal file
89
platform/features/hyperlink/test/HyperlinkControllerSpec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
70
platform/features/timeline/README.md
Normal file
70
platform/features/timeline/README.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
218
src/adapter/services/ExportImageService.js
Normal file
218
src/adapter/services/ExportImageService.js
Normal 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;
|
||||
}
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -46,7 +46,7 @@ define([
|
||||
StatusAPI
|
||||
) {
|
||||
return {
|
||||
TimeAPI: TimeAPI.default,
|
||||
TimeAPI: TimeAPI,
|
||||
ObjectAPI: ObjectAPI,
|
||||
CompositionAPI: CompositionAPI,
|
||||
TypeRegistry: TypeRegistry,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()"
|
||||
>
|
||||
|
||||
@@ -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 />'
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 = '---';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,7 +45,6 @@ export default class URLTimeSettingsSynchronizer {
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.updateTimeSettings();
|
||||
this.openmct.router.on('change:params', this.updateTimeSettings);
|
||||
|
||||
TIME_EVENTS.forEach(event => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -215,8 +215,7 @@ export default {
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
required: true
|
||||
},
|
||||
telemetry: {
|
||||
type: Array,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,7 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AlphanumericFormat',
|
||||
inject: ['openmct', 'objectPath'],
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.c-imagery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
> {{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<PopupMenu v-if="snapshots.length > 0"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -54,8 +54,7 @@ export default function PlanViewProvider(openmct) {
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
path: objectPath
|
||||
domainObject
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -53,8 +53,7 @@ export default function OverlayPlotViewProvider(openmct) {
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
path: objectPath
|
||||
domainObject
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user