Compare commits
4 Commits
fix-move-d
...
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.7-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
|
||||
|
||||
@@ -21,14 +21,28 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
"./src/AgentService"
|
||||
"./src/MCTDevice",
|
||||
"./src/AgentService",
|
||||
"./src/DeviceClassifier"
|
||||
], function (
|
||||
AgentService
|
||||
MCTDevice,
|
||||
AgentService,
|
||||
DeviceClassifier
|
||||
) {
|
||||
|
||||
return {
|
||||
name: "platform/commonUI/mobile",
|
||||
definition: {
|
||||
"extensions": {
|
||||
"directives": [
|
||||
{
|
||||
"key": "mctDevice",
|
||||
"implementation": MCTDevice,
|
||||
"depends": [
|
||||
"agentService"
|
||||
]
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"key": "agentService",
|
||||
@@ -37,6 +51,15 @@ define([
|
||||
"$window"
|
||||
]
|
||||
}
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"implementation": DeviceClassifier,
|
||||
"depends": [
|
||||
"agentService",
|
||||
"$document"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,122 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(["../../../../src/utils/agent/Agent.js"], function (Agent) {
|
||||
function AngularAgentServiceWrapper(window) {
|
||||
const AS = Agent.default;
|
||||
/**
|
||||
* Provides features which support variant behavior on mobile devices.
|
||||
*
|
||||
* @namespace platform/commonUI/mobile
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
return new AS(window);
|
||||
/**
|
||||
* The query service handles calls for browser and userAgent
|
||||
* info using a comparison between the userAgent and key
|
||||
* device names
|
||||
* @constructor
|
||||
* @param $window Angular-injected instance of the window
|
||||
* @memberof platform/commonUI/mobile
|
||||
*/
|
||||
function AgentService($window) {
|
||||
var userAgent = $window.navigator.userAgent,
|
||||
matches = userAgent.match(/iPad|iPhone|Android/i) || [];
|
||||
|
||||
this.userAgent = userAgent;
|
||||
this.mobileName = matches[0];
|
||||
this.$window = $window;
|
||||
this.touchEnabled = ($window.ontouchstart !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is on a mobile device.
|
||||
* @returns {boolean} true on mobile
|
||||
*/
|
||||
AgentService.prototype.isMobile = function () {
|
||||
return Boolean(this.mobileName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user is on a phone-sized mobile device.
|
||||
* @returns {boolean} true on a phone
|
||||
*/
|
||||
AgentService.prototype.isPhone = function () {
|
||||
if (this.isMobile()) {
|
||||
if (this.isAndroidTablet()) {
|
||||
return false;
|
||||
} else if (this.mobileName === 'iPad') {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user is on a tablet sized android device
|
||||
* @returns {boolean} true on an android tablet
|
||||
*/
|
||||
AgentService.prototype.isAndroidTablet = function () {
|
||||
if (this.mobileName === 'Android') {
|
||||
if (this.isPortrait() && window.innerWidth >= 768) {
|
||||
return true;
|
||||
} else if (this.isLandscape() && window.innerHeight >= 768) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user is on a tablet-sized mobile device.
|
||||
* @returns {boolean} true on a tablet
|
||||
*/
|
||||
AgentService.prototype.isTablet = function () {
|
||||
return (this.isMobile() && !this.isPhone() && this.mobileName !== 'Android') || (this.isMobile() && this.isAndroidTablet());
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user's device is in a portrait-style
|
||||
* orientation (display width is narrower than display height.)
|
||||
* @returns {boolean} true in portrait mode
|
||||
*/
|
||||
AgentService.prototype.isPortrait = function () {
|
||||
return this.$window.innerWidth < this.$window.innerHeight;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user's device is in a landscape-style
|
||||
* orientation (display width is greater than display height.)
|
||||
* @returns {boolean} true in landscape mode
|
||||
*/
|
||||
AgentService.prototype.isLandscape = function () {
|
||||
return !this.isPortrait();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user's device supports a touch interface.
|
||||
* @returns {boolean} true if touch is supported
|
||||
*/
|
||||
AgentService.prototype.isTouch = function () {
|
||||
return this.touchEnabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user agent matches a certain named device,
|
||||
* as indicated by checking for a case-insensitive substring
|
||||
* match.
|
||||
* @param {string} name the name to check for
|
||||
* @returns {boolean} true if the user agent includes that name
|
||||
*/
|
||||
AgentService.prototype.isBrowser = function (name) {
|
||||
name = name.toLowerCase();
|
||||
|
||||
return this.userAgent.toLowerCase().indexOf(name) !== -1;
|
||||
};
|
||||
|
||||
return AgentService;
|
||||
}
|
||||
|
||||
return AngularAgentServiceWrapper;
|
||||
});
|
||||
);
|
||||
|
||||
@@ -1,96 +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 AgentService from "./AgentService";
|
||||
|
||||
const TEST_USER_AGENTS = {
|
||||
DESKTOP:
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36",
|
||||
IPAD:
|
||||
"Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
|
||||
IPHONE:
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53"
|
||||
};
|
||||
|
||||
describe("The AgentService", function () {
|
||||
let testWindow;
|
||||
let agentService;
|
||||
|
||||
beforeEach(function () {
|
||||
testWindow = {
|
||||
innerWidth: 640,
|
||||
innerHeight: 480,
|
||||
navigator: {
|
||||
userAgent: TEST_USER_AGENTS.DESKTOP
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it("recognizes desktop devices as non-mobile", function () {
|
||||
testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP;
|
||||
agentService = new AgentService(testWindow);
|
||||
expect(agentService.isMobile()).toBeFalsy();
|
||||
expect(agentService.isPhone()).toBeFalsy();
|
||||
expect(agentService.isTablet()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("detects iPhones", function () {
|
||||
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE;
|
||||
agentService = new AgentService(testWindow);
|
||||
expect(agentService.isMobile()).toBeTruthy();
|
||||
expect(agentService.isPhone()).toBeTruthy();
|
||||
expect(agentService.isTablet()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("detects iPads", function () {
|
||||
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD;
|
||||
agentService = new AgentService(testWindow);
|
||||
expect(agentService.isMobile()).toBeTruthy();
|
||||
expect(agentService.isPhone()).toBeFalsy();
|
||||
expect(agentService.isTablet()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("detects display orientation", function () {
|
||||
agentService = new AgentService(testWindow);
|
||||
testWindow.innerWidth = 1024;
|
||||
testWindow.innerHeight = 400;
|
||||
expect(agentService.isPortrait()).toBeFalsy();
|
||||
expect(agentService.isLandscape()).toBeTruthy();
|
||||
testWindow.innerWidth = 400;
|
||||
testWindow.innerHeight = 1024;
|
||||
expect(agentService.isPortrait()).toBeTruthy();
|
||||
expect(agentService.isLandscape()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("detects touch support", function () {
|
||||
testWindow.ontouchstart = null;
|
||||
expect(new AgentService(testWindow).isTouch()).toBe(true);
|
||||
delete testWindow.ontouchstart;
|
||||
expect(new AgentService(testWindow).isTouch()).toBe(false);
|
||||
});
|
||||
|
||||
it("allows for checking browser type", function () {
|
||||
testWindow.navigator.userAgent = "Chromezilla Safarifox";
|
||||
agentService = new AgentService(testWindow);
|
||||
expect(agentService.isBrowser("Chrome")).toBe(true);
|
||||
expect(agentService.isBrowser("Firefox")).toBe(false);
|
||||
});
|
||||
});
|
||||
72
platform/commonUI/mobile/src/DeviceClassifier.js
Normal file
72
platform/commonUI/mobile/src/DeviceClassifier.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['./DeviceMatchers'],
|
||||
function (DeviceMatchers) {
|
||||
|
||||
/**
|
||||
* Runs at application startup and adds a subset of the following
|
||||
* CSS classes to the body of the document, depending on device
|
||||
* attributes:
|
||||
*
|
||||
* * `mobile`: Phones or tablets.
|
||||
* * `phone`: Phones specifically.
|
||||
* * `tablet`: Tablets specifically.
|
||||
* * `desktop`: Non-mobile devices.
|
||||
* * `portrait`: Devices in a portrait-style orientation.
|
||||
* * `landscape`: Devices in a landscape-style orientation.
|
||||
* * `touch`: Device supports touch events.
|
||||
*
|
||||
* @param {platform/commonUI/mobile.AgentService} agentService
|
||||
* the service used to examine the user agent
|
||||
* @param $document Angular's jqLite-wrapped document element
|
||||
* @constructor
|
||||
*/
|
||||
function MobileClassifier(agentService, $document) {
|
||||
var body = $document.find('body');
|
||||
|
||||
Object.keys(DeviceMatchers).forEach(function (key, index, array) {
|
||||
if (DeviceMatchers[key](agentService)) {
|
||||
body.addClass(key);
|
||||
}
|
||||
});
|
||||
|
||||
if (agentService.isMobile()) {
|
||||
var mediaQuery = window.matchMedia('(orientation: landscape)');
|
||||
|
||||
mediaQuery.addListener(function (event) {
|
||||
if (event.matches) {
|
||||
body.removeClass('portrait');
|
||||
body.addClass('landscape');
|
||||
} else {
|
||||
body.removeClass('landscape');
|
||||
body.addClass('portrait');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return MobileClassifier;
|
||||
|
||||
}
|
||||
);
|
||||
@@ -19,39 +19,40 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
define(function () {
|
||||
|
||||
/**
|
||||
* An object containing key-value pairs, where keys are symbolic of
|
||||
* device attributes, and values are functions that take the
|
||||
* `agent` as inputs and return boolean values indicating
|
||||
* whether or not the current device has these attributes.
|
||||
*
|
||||
* For internal use by the mobile support bundle.
|
||||
*
|
||||
* @memberof src/plugins/DeviceClassifier
|
||||
* @private
|
||||
*/
|
||||
|
||||
export default {
|
||||
mobile: function (agent) {
|
||||
return agent.isMobile();
|
||||
},
|
||||
phone: function (agent) {
|
||||
return agent.isPhone();
|
||||
},
|
||||
tablet: function (agent) {
|
||||
return agent.isTablet();
|
||||
},
|
||||
desktop: function (agent) {
|
||||
return !agent.isMobile();
|
||||
},
|
||||
portrait: function (agent) {
|
||||
return agent.isPortrait();
|
||||
},
|
||||
landscape: function (agent) {
|
||||
return agent.isLandscape();
|
||||
},
|
||||
touch: function (agent) {
|
||||
return agent.isTouch();
|
||||
}
|
||||
};
|
||||
/**
|
||||
* An object containing key-value pairs, where keys are symbolic of
|
||||
* device attributes, and values are functions that take the
|
||||
* `agentService` as inputs and return boolean values indicating
|
||||
* whether or not the current device has these attributes.
|
||||
*
|
||||
* For internal use by the mobile support bundle.
|
||||
*
|
||||
* @memberof platform/commonUI/mobile
|
||||
* @private
|
||||
*/
|
||||
return {
|
||||
mobile: function (agentService) {
|
||||
return agentService.isMobile();
|
||||
},
|
||||
phone: function (agentService) {
|
||||
return agentService.isPhone();
|
||||
},
|
||||
tablet: function (agentService) {
|
||||
return agentService.isTablet();
|
||||
},
|
||||
desktop: function (agentService) {
|
||||
return !agentService.isMobile();
|
||||
},
|
||||
portrait: function (agentService) {
|
||||
return agentService.isPortrait();
|
||||
},
|
||||
landscape: function (agentService) {
|
||||
return agentService.isLandscape();
|
||||
},
|
||||
touch: function (agentService) {
|
||||
return agentService.isTouch();
|
||||
}
|
||||
};
|
||||
});
|
||||
88
platform/commonUI/mobile/src/MCTDevice.js
Normal file
88
platform/commonUI/mobile/src/MCTDevice.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['./DeviceMatchers'],
|
||||
function (DeviceMatchers) {
|
||||
|
||||
/**
|
||||
* The `mct-device` directive, when applied as an attribute,
|
||||
* only includes the element when the device being used matches
|
||||
* a set of characteristics required.
|
||||
*
|
||||
* Required characteristics are given as space-separated strings
|
||||
* as the value to this attribute, e.g.:
|
||||
*
|
||||
* <span mct-device="mobile portrait">Hello world!</span>
|
||||
*
|
||||
* ...will only show Hello world! when viewed on a mobile device
|
||||
* in the portrait orientation.
|
||||
*
|
||||
* Valid device characteristics to detect are:
|
||||
*
|
||||
* * `mobile`: Phones or tablets.
|
||||
* * `phone`: Phones specifically.
|
||||
* * `tablet`: Tablets specifically.
|
||||
* * `desktop`: Non-mobile devices.
|
||||
* * `portrait`: Devices in a portrait-style orientation.
|
||||
* * `landscape`: Devices in a landscape-style orientation.
|
||||
* * `touch`: Device supports touch events.
|
||||
*
|
||||
* @param {AgentService} agentService used to detect device type
|
||||
* based on information about the user agent
|
||||
*/
|
||||
function MCTDevice(agentService) {
|
||||
|
||||
function deviceMatches(tokens) {
|
||||
tokens = tokens || "";
|
||||
|
||||
return tokens.split(" ").every(function (token) {
|
||||
var fn = DeviceMatchers[token];
|
||||
|
||||
return fn && fn(agentService);
|
||||
});
|
||||
}
|
||||
|
||||
function link(scope, element, attrs, ctrl, transclude) {
|
||||
if (deviceMatches(attrs.mctDevice)) {
|
||||
transclude(function (clone) {
|
||||
element.replaceWith(clone);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
link: link,
|
||||
// We are transcluding the whole element (like ng-if)
|
||||
transclude: 'element',
|
||||
// 1 more than ng-if
|
||||
priority: 601,
|
||||
// Also terminal, since element will be transcluded
|
||||
terminal: true,
|
||||
// Only apply as an attribute
|
||||
restrict: "A"
|
||||
};
|
||||
}
|
||||
|
||||
return MCTDevice;
|
||||
}
|
||||
);
|
||||
99
platform/commonUI/mobile/test/AgentServiceSpec.js
Normal file
99
platform/commonUI/mobile/test/AgentServiceSpec.js
Normal file
@@ -0,0 +1,99 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/AgentService"],
|
||||
function (AgentService) {
|
||||
|
||||
var TEST_USER_AGENTS = {
|
||||
DESKTOP: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36",
|
||||
IPAD: "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
|
||||
IPHONE: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53"
|
||||
};
|
||||
|
||||
describe("The AgentService", function () {
|
||||
var testWindow, agentService;
|
||||
|
||||
beforeEach(function () {
|
||||
testWindow = {
|
||||
innerWidth: 640,
|
||||
innerHeight: 480,
|
||||
navigator: {
|
||||
userAgent: TEST_USER_AGENTS.DESKTOP
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it("recognizes desktop devices as non-mobile", function () {
|
||||
testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP;
|
||||
agentService = new AgentService(testWindow);
|
||||
expect(agentService.isMobile()).toBeFalsy();
|
||||
expect(agentService.isPhone()).toBeFalsy();
|
||||
expect(agentService.isTablet()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("detects iPhones", function () {
|
||||
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE;
|
||||
agentService = new AgentService(testWindow);
|
||||
expect(agentService.isMobile()).toBeTruthy();
|
||||
expect(agentService.isPhone()).toBeTruthy();
|
||||
expect(agentService.isTablet()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("detects iPads", function () {
|
||||
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD;
|
||||
agentService = new AgentService(testWindow);
|
||||
expect(agentService.isMobile()).toBeTruthy();
|
||||
expect(agentService.isPhone()).toBeFalsy();
|
||||
expect(agentService.isTablet()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("detects display orientation", function () {
|
||||
agentService = new AgentService(testWindow);
|
||||
testWindow.innerWidth = 1024;
|
||||
testWindow.innerHeight = 400;
|
||||
expect(agentService.isPortrait()).toBeFalsy();
|
||||
expect(agentService.isLandscape()).toBeTruthy();
|
||||
testWindow.innerWidth = 400;
|
||||
testWindow.innerHeight = 1024;
|
||||
expect(agentService.isPortrait()).toBeTruthy();
|
||||
expect(agentService.isLandscape()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("detects touch support", function () {
|
||||
testWindow.ontouchstart = null;
|
||||
expect(new AgentService(testWindow).isTouch())
|
||||
.toBe(true);
|
||||
delete testWindow.ontouchstart;
|
||||
expect(new AgentService(testWindow).isTouch())
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it("allows for checking browser type", function () {
|
||||
testWindow.navigator.userAgent = "Chromezilla Safarifox";
|
||||
agentService = new AgentService(testWindow);
|
||||
expect(agentService.isBrowser("Chrome")).toBe(true);
|
||||
expect(agentService.isBrowser("Firefox")).toBe(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
109
platform/commonUI/mobile/test/DeviceClassifierSpec.js
Normal file
109
platform/commonUI/mobile/test/DeviceClassifierSpec.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/DeviceClassifier", "../src/DeviceMatchers"],
|
||||
function (DeviceClassifier, DeviceMatchers) {
|
||||
|
||||
var AGENT_SERVICE_METHODS = [
|
||||
'isMobile',
|
||||
'isPhone',
|
||||
'isTablet',
|
||||
'isPortrait',
|
||||
'isLandscape',
|
||||
'isTouch'
|
||||
],
|
||||
TEST_PERMUTATIONS = [
|
||||
['isMobile', 'isPhone', 'isTouch', 'isPortrait'],
|
||||
['isMobile', 'isPhone', 'isTouch', 'isLandscape'],
|
||||
['isMobile', 'isTablet', 'isTouch', 'isPortrait'],
|
||||
['isMobile', 'isTablet', 'isTouch', 'isLandscape'],
|
||||
['isTouch'],
|
||||
[]
|
||||
];
|
||||
|
||||
describe("DeviceClassifier", function () {
|
||||
var mockAgentService,
|
||||
mockDocument,
|
||||
mockBody;
|
||||
|
||||
beforeEach(function () {
|
||||
mockAgentService = jasmine.createSpyObj(
|
||||
'agentService',
|
||||
AGENT_SERVICE_METHODS
|
||||
);
|
||||
mockDocument = jasmine.createSpyObj(
|
||||
'$document',
|
||||
['find']
|
||||
);
|
||||
mockBody = jasmine.createSpyObj(
|
||||
'body',
|
||||
['addClass']
|
||||
);
|
||||
mockDocument.find.and.callFake(function (sel) {
|
||||
return sel === 'body' && mockBody;
|
||||
});
|
||||
AGENT_SERVICE_METHODS.forEach(function (m) {
|
||||
mockAgentService[m].and.returnValue(false);
|
||||
});
|
||||
});
|
||||
|
||||
TEST_PERMUTATIONS.forEach(function (trueMethods) {
|
||||
var summary = trueMethods.length === 0
|
||||
? "device has no detected characteristics"
|
||||
: "device " + (trueMethods.join(", "));
|
||||
|
||||
describe("when " + summary, function () {
|
||||
var classifier; // eslint-disable-line
|
||||
|
||||
beforeEach(function () {
|
||||
trueMethods.forEach(function (m) {
|
||||
mockAgentService[m].and.returnValue(true);
|
||||
});
|
||||
classifier = new DeviceClassifier(
|
||||
mockAgentService,
|
||||
mockDocument
|
||||
);
|
||||
});
|
||||
|
||||
it("adds classes for matching, detected characteristics", function () {
|
||||
Object.keys(DeviceMatchers).filter(function (m) {
|
||||
return DeviceMatchers[m](mockAgentService);
|
||||
}).forEach(function (key) {
|
||||
expect(mockBody.addClass)
|
||||
.toHaveBeenCalledWith(key);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not add classes for non-matching characteristics", function () {
|
||||
Object.keys(DeviceMatchers).filter(function (m) {
|
||||
return !DeviceMatchers[m](mockAgentService);
|
||||
}).forEach(function (key) {
|
||||
expect(mockBody.addClass)
|
||||
.not.toHaveBeenCalledWith(key);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
78
platform/commonUI/mobile/test/DeviceMatchersSpec.js
Normal file
78
platform/commonUI/mobile/test/DeviceMatchersSpec.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/DeviceMatchers"],
|
||||
function (DeviceMatchers) {
|
||||
|
||||
describe("DeviceMatchers", function () {
|
||||
var mockAgentService;
|
||||
|
||||
beforeEach(function () {
|
||||
mockAgentService = jasmine.createSpyObj(
|
||||
'agentService',
|
||||
[
|
||||
'isMobile',
|
||||
'isPhone',
|
||||
'isTablet',
|
||||
'isPortrait',
|
||||
'isLandscape',
|
||||
'isTouch'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("detects when a device is a desktop device", function () {
|
||||
mockAgentService.isMobile.and.returnValue(false);
|
||||
expect(DeviceMatchers.desktop(mockAgentService))
|
||||
.toBe(true);
|
||||
mockAgentService.isMobile.and.returnValue(true);
|
||||
expect(DeviceMatchers.desktop(mockAgentService))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
function method(deviceType) {
|
||||
return "is" + deviceType[0].toUpperCase() + deviceType.slice(1);
|
||||
}
|
||||
|
||||
[
|
||||
"mobile",
|
||||
"phone",
|
||||
"tablet",
|
||||
"landscape",
|
||||
"portrait",
|
||||
"landscape",
|
||||
"touch"
|
||||
].forEach(function (deviceType) {
|
||||
it("detects when a device is a " + deviceType + " device", function () {
|
||||
mockAgentService[method(deviceType)].and.returnValue(true);
|
||||
expect(DeviceMatchers[deviceType](mockAgentService))
|
||||
.toBe(true);
|
||||
mockAgentService[method(deviceType)].and.returnValue(false);
|
||||
expect(DeviceMatchers[deviceType](mockAgentService))
|
||||
.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
168
platform/commonUI/mobile/test/MCTDeviceSpec.js
Normal file
168
platform/commonUI/mobile/test/MCTDeviceSpec.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['../src/MCTDevice'],
|
||||
function (MCTDevice) {
|
||||
|
||||
var JQLITE_METHODS = ['replaceWith'];
|
||||
|
||||
describe("The mct-device directive", function () {
|
||||
var mockAgentService,
|
||||
mockTransclude,
|
||||
mockElement,
|
||||
mockClone,
|
||||
testAttrs,
|
||||
directive;
|
||||
|
||||
function link() {
|
||||
directive.link(null, mockElement, testAttrs, null, mockTransclude);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockAgentService = jasmine.createSpyObj(
|
||||
"agentService",
|
||||
["isMobile", "isPhone", "isTablet", "isPortrait", "isLandscape"]
|
||||
);
|
||||
mockTransclude = jasmine.createSpy("$transclude");
|
||||
mockElement = jasmine.createSpyObj(name, JQLITE_METHODS);
|
||||
mockClone = jasmine.createSpyObj(name, JQLITE_METHODS);
|
||||
|
||||
mockTransclude.and.callFake(function (fn) {
|
||||
fn(mockClone);
|
||||
});
|
||||
|
||||
// Look desktop-like by default
|
||||
mockAgentService.isLandscape.and.returnValue(true);
|
||||
|
||||
testAttrs = {};
|
||||
|
||||
directive = new MCTDevice(mockAgentService);
|
||||
});
|
||||
|
||||
function expectInclusion() {
|
||||
expect(mockElement.replaceWith)
|
||||
.toHaveBeenCalledWith(mockClone);
|
||||
}
|
||||
|
||||
function expectExclusion() {
|
||||
expect(mockElement.replaceWith).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
it("is applicable at the attribute level", function () {
|
||||
expect(directive.restrict).toEqual("A");
|
||||
});
|
||||
|
||||
it("transcludes at the element level", function () {
|
||||
expect(directive.transclude).toEqual('element');
|
||||
});
|
||||
|
||||
it("has a greater priority number than ng-if", function () {
|
||||
expect(directive.priority > 600).toBeTruthy();
|
||||
});
|
||||
|
||||
it("restricts element inclusion for mobile devices", function () {
|
||||
testAttrs.mctDevice = "mobile";
|
||||
link();
|
||||
expectExclusion();
|
||||
|
||||
mockAgentService.isMobile.and.returnValue(true);
|
||||
link();
|
||||
expectInclusion();
|
||||
});
|
||||
|
||||
it("restricts element inclusion for tablet devices", function () {
|
||||
testAttrs.mctDevice = "tablet";
|
||||
mockAgentService.isMobile.and.returnValue(true);
|
||||
link();
|
||||
expectExclusion();
|
||||
|
||||
mockAgentService.isTablet.and.returnValue(true);
|
||||
link();
|
||||
expectInclusion();
|
||||
});
|
||||
|
||||
it("restricts element inclusion for phone devices", function () {
|
||||
testAttrs.mctDevice = "phone";
|
||||
mockAgentService.isMobile.and.returnValue(true);
|
||||
link();
|
||||
expectExclusion();
|
||||
|
||||
mockAgentService.isPhone.and.returnValue(true);
|
||||
link();
|
||||
expectInclusion();
|
||||
});
|
||||
|
||||
it("restricts element inclusion for desktop devices", function () {
|
||||
testAttrs.mctDevice = "desktop";
|
||||
mockAgentService.isMobile.and.returnValue(true);
|
||||
link();
|
||||
expectExclusion();
|
||||
|
||||
mockAgentService.isMobile.and.returnValue(false);
|
||||
link();
|
||||
expectInclusion();
|
||||
});
|
||||
|
||||
it("restricts element inclusion for portrait orientation", function () {
|
||||
testAttrs.mctDevice = "portrait";
|
||||
link();
|
||||
expectExclusion();
|
||||
|
||||
mockAgentService.isPortrait.and.returnValue(true);
|
||||
link();
|
||||
expectInclusion();
|
||||
});
|
||||
|
||||
it("restricts element inclusion for landscape orientation", function () {
|
||||
testAttrs.mctDevice = "landscape";
|
||||
mockAgentService.isLandscape.and.returnValue(false);
|
||||
mockAgentService.isPortrait.and.returnValue(true);
|
||||
link();
|
||||
expectExclusion();
|
||||
|
||||
mockAgentService.isLandscape.and.returnValue(true);
|
||||
link();
|
||||
expectInclusion();
|
||||
});
|
||||
|
||||
it("allows multiple device characteristics to be requested", function () {
|
||||
// Won't try to test every permutation here, just
|
||||
// make sure the multi-characteristic feature has support.
|
||||
testAttrs.mctDevice = "portrait mobile";
|
||||
link();
|
||||
// Neither portrait nor mobile, not called
|
||||
expectExclusion();
|
||||
|
||||
mockAgentService.isPortrait.and.returnValue(true);
|
||||
link();
|
||||
|
||||
// Was portrait, but not mobile, so no
|
||||
expectExclusion();
|
||||
|
||||
mockAgentService.isMobile.and.returnValue(true);
|
||||
link();
|
||||
expectInclusion();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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
|
||||
@@ -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,10 +283,8 @@ 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());
|
||||
this.install(this.plugins.DeviceClassifier());
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
@@ -437,8 +434,6 @@ define([
|
||||
Browse(this);
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', this.destroy);
|
||||
|
||||
this.router.start();
|
||||
this.emit('start');
|
||||
}.bind(this));
|
||||
@@ -462,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,24 +57,25 @@ 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) {
|
||||
return this._actionCollections.get(view);
|
||||
let cachedActionCollection = this._actionCollections.get(view);
|
||||
|
||||
return cachedActionCollection;
|
||||
}
|
||||
|
||||
_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);
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Runs at application startup and adds a subset of the following
|
||||
* CSS classes to the body of the document, depending on device
|
||||
* attributes:
|
||||
*
|
||||
* * `mobile`: Phones or tablets.
|
||||
* * `phone`: Phones specifically.
|
||||
* * `tablet`: Tablets specifically.
|
||||
* * `desktop`: Non-mobile devices.
|
||||
* * `portrait`: Devices in a portrait-style orientation.
|
||||
* * `landscape`: Devices in a landscape-style orientation.
|
||||
* * `touch`: Device supports touch events.
|
||||
*
|
||||
* @param {utils/agent/Agent} agent
|
||||
* the service used to examine the user agent
|
||||
* @param document the HTML DOM document object
|
||||
* @constructor
|
||||
*/
|
||||
import DeviceMatchers from "./DeviceMatchers";
|
||||
|
||||
export default (agent, document) => {
|
||||
const body = document.body;
|
||||
|
||||
Object.keys(DeviceMatchers).forEach((key, index, array) => {
|
||||
if (DeviceMatchers[key](agent)) {
|
||||
body.classList.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
if (agent.isMobile()) {
|
||||
const mediaQuery = window.matchMedia("(orientation: landscape)");
|
||||
function eventHandler(event) {
|
||||
console.log("changed");
|
||||
if (event.matches) {
|
||||
body.classList.remove("portrait");
|
||||
body.classList.add("landscape");
|
||||
} else {
|
||||
body.classList.remove("landscape");
|
||||
body.classList.add("portrait");
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener(`change`, eventHandler);
|
||||
} else {
|
||||
// Deprecated 'MediaQueryList' API, <Safari 14, IE, <Edge 16
|
||||
mediaQuery.addListener(eventHandler);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,105 +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 DeviceClassifier from "./DeviceClassifier";
|
||||
import DeviceMatchers from "./DeviceMatchers";
|
||||
|
||||
const AGENT_METHODS = [
|
||||
"isMobile",
|
||||
"isPhone",
|
||||
"isTablet",
|
||||
"isPortrait",
|
||||
"isLandscape",
|
||||
"isTouch"
|
||||
];
|
||||
const TEST_PERMUTATIONS = [
|
||||
["isMobile", "isPhone", "isTouch", "isPortrait"],
|
||||
["isMobile", "isPhone", "isTouch", "isLandscape"],
|
||||
["isMobile", "isTablet", "isTouch", "isPortrait"],
|
||||
["isMobile", "isTablet", "isTouch", "isLandscape"],
|
||||
["isTouch"],
|
||||
[]
|
||||
];
|
||||
|
||||
describe("DeviceClassifier", function () {
|
||||
let mockAgent;
|
||||
let mockDocument;
|
||||
let mockClassList;
|
||||
|
||||
beforeEach(function () {
|
||||
mockAgent = jasmine.createSpyObj(
|
||||
"agent",
|
||||
AGENT_METHODS
|
||||
);
|
||||
|
||||
mockClassList = jasmine.createSpyObj("classList", ["add"]);
|
||||
|
||||
mockDocument = jasmine.createSpyObj(
|
||||
"document",
|
||||
{},
|
||||
{ body: { classList: mockClassList } }
|
||||
);
|
||||
|
||||
AGENT_METHODS.forEach(function (m) {
|
||||
mockAgent[m].and.returnValue(false);
|
||||
});
|
||||
});
|
||||
|
||||
TEST_PERMUTATIONS.forEach(function (trueMethods) {
|
||||
const summary =
|
||||
trueMethods.length === 0
|
||||
? "device has no detected characteristics"
|
||||
: "device " + trueMethods.join(", ");
|
||||
|
||||
describe("when " + summary, function () {
|
||||
beforeEach(function () {
|
||||
trueMethods.forEach(function (m) {
|
||||
mockAgent[m].and.returnValue(true);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
DeviceClassifier(mockAgent, mockDocument);
|
||||
});
|
||||
|
||||
it("adds classes for matching, detected characteristics", function () {
|
||||
Object.keys(DeviceMatchers)
|
||||
.filter(function (m) {
|
||||
return DeviceMatchers[m](mockAgent);
|
||||
})
|
||||
.forEach(function (key) {
|
||||
expect(mockDocument.body.classList.add).toHaveBeenCalledWith(key);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not add classes for non-matching characteristics", function () {
|
||||
Object.keys(DeviceMatchers)
|
||||
.filter(function (m) {
|
||||
return !DeviceMatchers[m](mockAgent);
|
||||
})
|
||||
.forEach(function (key) {
|
||||
expect(mockDocument.body.classList.add).not.toHaveBeenCalledWith(
|
||||
key
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,65 +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 DeviceMatchers from "./DeviceMatchers";
|
||||
|
||||
describe("DeviceMatchers", function () {
|
||||
let mockAgent;
|
||||
|
||||
beforeEach(function () {
|
||||
mockAgent = jasmine.createSpyObj("agent", [
|
||||
"isMobile",
|
||||
"isPhone",
|
||||
"isTablet",
|
||||
"isPortrait",
|
||||
"isLandscape",
|
||||
"isTouch"
|
||||
]);
|
||||
});
|
||||
|
||||
it("detects when a device is a desktop device", function () {
|
||||
mockAgent.isMobile.and.returnValue(false);
|
||||
expect(DeviceMatchers.desktop(mockAgent)).toBe(true);
|
||||
mockAgent.isMobile.and.returnValue(true);
|
||||
expect(DeviceMatchers.desktop(mockAgent)).toBe(false);
|
||||
});
|
||||
|
||||
function method(deviceType) {
|
||||
return "is" + deviceType[0].toUpperCase() + deviceType.slice(1);
|
||||
}
|
||||
|
||||
[
|
||||
"mobile",
|
||||
"phone",
|
||||
"tablet",
|
||||
"landscape",
|
||||
"portrait",
|
||||
"landscape",
|
||||
"touch"
|
||||
].forEach(function (deviceType) {
|
||||
it("detects when a device is a " + deviceType + " device", function () {
|
||||
mockAgent[method(deviceType)].and.returnValue(true);
|
||||
expect(DeviceMatchers[deviceType](mockAgent)).toBe(true);
|
||||
mockAgent[method(deviceType)].and.returnValue(false);
|
||||
expect(DeviceMatchers[deviceType](mockAgent)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -25,16 +25,16 @@
|
||||
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
|
||||
>
|
||||
<plot-legend :cursor-locked="!!lockHighlightPoint"
|
||||
:series="seriesModels"
|
||||
:series="config.series.models"
|
||||
:highlights="highlights"
|
||||
:legend="legend"
|
||||
:legend="config.legend"
|
||||
@legendHoverChanged="legendHoverChanged"
|
||||
/>
|
||||
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
|
||||
<y-axis v-if="seriesModels.length > 0"
|
||||
<y-axis v-if="config.series.models.length > 0"
|
||||
:tick-width="tickWidth"
|
||||
:single-series="seriesModels.length === 1"
|
||||
:series-model="seriesModels[0]"
|
||||
:single-series="config.series.models.length === 1"
|
||||
:series-model="config.series.models[0]"
|
||||
@yKeyChanged="setYAxisKey"
|
||||
@tickWidthChanged="onTickWidthChange"
|
||||
/>
|
||||
@@ -141,8 +141,8 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<x-axis v-if="seriesModels.length > 0 && !options.compact"
|
||||
:series-model="seriesModels[0]"
|
||||
<x-axis v-if="config.series.models.length > 0 && !options.compact"
|
||||
:series-model="config.series.models[0]"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -213,8 +213,7 @@ export default {
|
||||
plotHistory: [],
|
||||
selectedXKeyOption: {},
|
||||
xKeyOptions: [],
|
||||
seriesModels: [],
|
||||
legend: {},
|
||||
config: {},
|
||||
pending: 0,
|
||||
isRealTime: this.openmct.time.clock() !== undefined,
|
||||
loaded: false,
|
||||
@@ -240,13 +239,18 @@ export default {
|
||||
watch: {
|
||||
plotTickWidth(newTickWidth) {
|
||||
this.onTickWidthChange(newTickWidth, true);
|
||||
},
|
||||
gridLines(newGridLines) {
|
||||
this.setGridLinesVisibility(newGridLines);
|
||||
},
|
||||
cursorGuide(newCursorGuide) {
|
||||
this.setCursorGuideVisibility(newCursorGuide);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
|
||||
this.config = this.getConfig();
|
||||
this.legend = this.config.legend;
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
@@ -286,18 +290,14 @@ export default {
|
||||
config = new PlotConfigurationModel({
|
||||
id: configId,
|
||||
domainObject: this.domainObject,
|
||||
openmct: this.openmct,
|
||||
callback: (data) => {
|
||||
this.data = data;
|
||||
}
|
||||
openmct: this.openmct
|
||||
});
|
||||
configStore.add(configId, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
addSeries(series, index) {
|
||||
this.$set(this.seriesModels, index, series);
|
||||
addSeries(series) {
|
||||
this.listenTo(series, 'change:xKey', (xKey) => {
|
||||
this.setDisplayRange(series, xKey);
|
||||
}, this);
|
||||
@@ -323,8 +323,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
|
||||
|
||||
this.startLoading();
|
||||
const options = {
|
||||
size: this.$parent.$refs.plotWrapper.offsetWidth,
|
||||
@@ -379,8 +377,11 @@ export default {
|
||||
},
|
||||
|
||||
stopLoading() {
|
||||
this.pending -= 1;
|
||||
this.updateLoading();
|
||||
//TODO: Is Vue.$nextTick ok to replace $scope.$evalAsync?
|
||||
this.$nextTick().then(() => {
|
||||
this.pending -= 1;
|
||||
this.updateLoading();
|
||||
});
|
||||
},
|
||||
|
||||
updateLoading() {
|
||||
@@ -426,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
|
||||
@@ -506,7 +504,7 @@ export default {
|
||||
},
|
||||
|
||||
initialize() {
|
||||
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
|
||||
_.debounce(this.handleWindowResize, 400);
|
||||
this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);
|
||||
this.plotContainerResizeObserver.observe(this.$parent.$refs.plotWrapper);
|
||||
|
||||
@@ -622,7 +620,7 @@ export default {
|
||||
this.config.series.models.forEach(series => delete series.closest);
|
||||
} else {
|
||||
this.highlights = this.config.series.models
|
||||
.filter(series => series.getSeriesData().length > 0)
|
||||
.filter(series => series.data.length > 0)
|
||||
.map(series => {
|
||||
series.closest = series.nearestPoint(point);
|
||||
|
||||
@@ -926,8 +924,16 @@ export default {
|
||||
this.userViewportChangeEnd();
|
||||
},
|
||||
|
||||
setCursorGuideVisibility(cursorGuide) {
|
||||
this.cursorGuide = cursorGuide === true;
|
||||
},
|
||||
|
||||
setGridLinesVisibility(gridLines) {
|
||||
this.gridLines = gridLines === true;
|
||||
},
|
||||
|
||||
setYAxisKey(yKey) {
|
||||
this.config.series.models[0].set('yKey', yKey);
|
||||
this.config.series.models[0].emit('change:yKey', yKey);
|
||||
},
|
||||
|
||||
pause() {
|
||||
|
||||
@@ -72,8 +72,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventHelpers from './lib/eventHelpers';
|
||||
import ImageExporter from '../../exporters/ImageExporter';
|
||||
import eventHelpers from "./lib/eventHelpers";
|
||||
import MctPlot from './MctPlot.vue';
|
||||
|
||||
export default {
|
||||
@@ -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) {
|
||||
|
||||
@@ -106,9 +106,8 @@ export default {
|
||||
},
|
||||
toggleXKeyOption() {
|
||||
const selectedXKey = this.selectedXKeyOptionKey;
|
||||
const seriesData = this.seriesModel.getSeriesData();
|
||||
const dataForSelectedXKey = seriesData
|
||||
? seriesData[0][selectedXKey]
|
||||
const dataForSelectedXKey = this.seriesModel.data
|
||||
? this.seriesModel.data[0][selectedXKey]
|
||||
: undefined;
|
||||
|
||||
if (dataForSelectedXKey !== undefined) {
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class MCTChartAlarmPointSet {
|
||||
this.listenTo(series, 'reset', this.reset, this);
|
||||
this.listenTo(series, 'destroy', this.destroy, this);
|
||||
|
||||
this.series.getSeriesData().forEach(function (point, index) {
|
||||
series.data.forEach(function (point, index) {
|
||||
this.append(point, index, series);
|
||||
}, this);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class MCTChartSeriesElement {
|
||||
this.listenTo(series, 'remove', this.remove, this);
|
||||
this.listenTo(series, 'reset', this.reset, this);
|
||||
this.listenTo(series, 'destroy', this.destroy, this);
|
||||
this.series.getSeriesData().forEach(function (point, index) {
|
||||
series.data.forEach(function (point, index) {
|
||||
this.append(point, index, series);
|
||||
}, this);
|
||||
}
|
||||
@@ -133,7 +133,7 @@ export default class MCTChartSeriesElement {
|
||||
this.buffer = new Float32Array(20000);
|
||||
this.count = 0;
|
||||
if (this.offset.x) {
|
||||
this.series.getSeriesData().forEach(function (point, index) {
|
||||
this.series.data.forEach(function (point, index) {
|
||||
this.append(point, index, this.series);
|
||||
}, this);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,6 @@ export default class PlotConfigurationModel extends Model {
|
||||
updateDomainObject(domainObject) {
|
||||
this.set('domainObject', domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all objects and remove all listeners.
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import _ from 'lodash';
|
||||
import Model from "./Model";
|
||||
import { MARKER_SHAPES } from '../draw/MarkerShapes';
|
||||
import configStore from "../configuration/configStore";
|
||||
|
||||
/**
|
||||
* Plot series handle interpreting telemetry metadata for a single telemetry
|
||||
@@ -63,6 +62,7 @@ import configStore from "../configuration/configStore";
|
||||
export default class PlotSeries extends Model {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.data = [];
|
||||
|
||||
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
|
||||
this.listenTo(this, 'change:yKey', this.onYKeyChange, this);
|
||||
@@ -115,8 +115,6 @@ export default class PlotSeries extends Model {
|
||||
this.openmct = options.openmct;
|
||||
this.domainObject = options.domainObject;
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.dataStoreId = `data-${options.collection.plot.id}-${this.keyString}`;
|
||||
this.updateSeriesData([]);
|
||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
|
||||
this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject);
|
||||
this.limits = [];
|
||||
@@ -184,8 +182,7 @@ export default class PlotSeries extends Model {
|
||||
.telemetry
|
||||
.request(this.domainObject, options)
|
||||
.then(function (points) {
|
||||
const data = this.getSeriesData();
|
||||
const newPoints = _(data)
|
||||
const newPoints = _(this.data)
|
||||
.concat(points)
|
||||
.sortBy(this.getXVal)
|
||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||
@@ -239,7 +236,7 @@ export default class PlotSeries extends Model {
|
||||
*/
|
||||
resetStats() {
|
||||
this.unset('stats');
|
||||
this.getSeriesData().forEach(this.updateStats, this);
|
||||
this.data.forEach(this.updateStats, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,7 +244,7 @@ export default class PlotSeries extends Model {
|
||||
* data to series after reset.
|
||||
*/
|
||||
reset(newData) {
|
||||
this.updateSeriesData([]);
|
||||
this.data = [];
|
||||
this.resetStats();
|
||||
this.emit('reset');
|
||||
if (newData) {
|
||||
@@ -261,9 +258,8 @@ export default class PlotSeries extends Model {
|
||||
*/
|
||||
nearestPoint(xValue) {
|
||||
const insertIndex = this.sortedIndex(xValue);
|
||||
const data = this.getSeriesData();
|
||||
const lowPoint = data[insertIndex - 1];
|
||||
const highPoint = data[insertIndex];
|
||||
const lowPoint = this.data[insertIndex - 1];
|
||||
const highPoint = this.data[insertIndex];
|
||||
const indexVal = this.getXVal(xValue);
|
||||
const lowDistance = lowPoint
|
||||
? indexVal - this.getXVal(lowPoint)
|
||||
@@ -296,7 +292,7 @@ export default class PlotSeries extends Model {
|
||||
* @private
|
||||
*/
|
||||
sortedIndex(point) {
|
||||
return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal);
|
||||
return _.sortedIndexBy(this.data, point, this.getXVal);
|
||||
}
|
||||
/**
|
||||
* Update min/max stats for the series.
|
||||
@@ -350,10 +346,9 @@ export default class PlotSeries extends Model {
|
||||
* a point to the end without dupe checking.
|
||||
*/
|
||||
add(point, appendOnly) {
|
||||
let data = this.getSeriesData();
|
||||
let insertIndex = data.length;
|
||||
let insertIndex = this.data.length;
|
||||
const currentYVal = this.getYVal(point);
|
||||
const lastYVal = this.getYVal(data[insertIndex - 1]);
|
||||
const lastYVal = this.getYVal(this.data[insertIndex - 1]);
|
||||
|
||||
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
|
||||
console.warn('[Plot] Invalid Y Values detected');
|
||||
@@ -363,19 +358,18 @@ export default class PlotSeries extends Model {
|
||||
|
||||
if (!appendOnly) {
|
||||
insertIndex = this.sortedIndex(point);
|
||||
if (this.getXVal(data[insertIndex]) === this.getXVal(point)) {
|
||||
if (this.getXVal(this.data[insertIndex]) === this.getXVal(point)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getXVal(data[insertIndex - 1]) === this.getXVal(point)) {
|
||||
if (this.getXVal(this.data[insertIndex - 1]) === this.getXVal(point)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateStats(point);
|
||||
point.mctLimitState = this.evaluate(point);
|
||||
data.splice(insertIndex, 0, point);
|
||||
this.updateSeriesData(data);
|
||||
this.data.splice(insertIndex, 0, point);
|
||||
this.emit('add', point, insertIndex, this);
|
||||
}
|
||||
|
||||
@@ -392,10 +386,8 @@ export default class PlotSeries extends Model {
|
||||
* @private
|
||||
*/
|
||||
remove(point) {
|
||||
let data = this.getSeriesData();
|
||||
const index = data.indexOf(point);
|
||||
data.splice(index, 1);
|
||||
this.updateSeriesData(data);
|
||||
const index = this.data.indexOf(point);
|
||||
this.data.splice(index, 1);
|
||||
this.emit('remove', point, index, this);
|
||||
}
|
||||
/**
|
||||
@@ -411,16 +403,14 @@ export default class PlotSeries extends Model {
|
||||
purgeRecordsOutsideRange(range) {
|
||||
const startIndex = this.sortedIndex(range.min);
|
||||
const endIndex = this.sortedIndex(range.max) + 1;
|
||||
let data = this.getSeriesData();
|
||||
const pointsToRemove = startIndex + (data.length - endIndex + 1);
|
||||
const pointsToRemove = startIndex + (this.data.length - endIndex + 1);
|
||||
if (pointsToRemove > 0) {
|
||||
if (pointsToRemove < 1000) {
|
||||
data.slice(0, startIndex).forEach(this.remove, this);
|
||||
data.slice(endIndex, data.length).forEach(this.remove, this);
|
||||
this.updateSeriesData(data);
|
||||
this.data.slice(0, startIndex).forEach(this.remove, this);
|
||||
this.data.slice(endIndex, this.data.length).forEach(this.remove, this);
|
||||
this.resetStats();
|
||||
} else {
|
||||
const newData = this.getSeriesData().slice(startIndex, endIndex);
|
||||
const newData = this.data.slice(startIndex, endIndex);
|
||||
this.reset(newData);
|
||||
}
|
||||
}
|
||||
@@ -451,13 +441,12 @@ export default class PlotSeries extends Model {
|
||||
}
|
||||
}
|
||||
getDisplayRange(xKey) {
|
||||
const unsortedData = this.getSeriesData();
|
||||
this.updateSeriesData([]);
|
||||
const unsortedData = this.data;
|
||||
this.data = [];
|
||||
unsortedData.forEach(point => this.add(point, false));
|
||||
|
||||
let data = this.getSeriesData();
|
||||
const minValue = this.getXVal(data[0]);
|
||||
const maxValue = this.getXVal(data[data.length - 1]);
|
||||
const minValue = this.getXVal(this.data[0]);
|
||||
const maxValue = this.getXVal(this.data[this.data.length - 1]);
|
||||
|
||||
return {
|
||||
min: minValue,
|
||||
@@ -481,18 +470,4 @@ export default class PlotSeries extends Model {
|
||||
|
||||
return this.get('name') + (unit ? ' ' + unit : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the series data with the given value.
|
||||
*/
|
||||
updateSeriesData(data) {
|
||||
configStore.add(this.dataStoreId, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the series data with the given value.
|
||||
*/
|
||||
getSeriesData() {
|
||||
return configStore.get(this.dataStoreId) || [];
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user