Compare commits
	
		
			15 Commits
		
	
	
		
			fix-move-d
			...
			1.7.5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e3999fd657 | ||
| 
						 | 
					51d4ad58c7 | ||
| 
						 | 
					36fc4929fc | ||
| 
						 | 
					0f848d9f4c | ||
| 
						 | 
					b6cc1924aa | ||
| 
						 | 
					3e97f32dba | ||
| 
						 | 
					558b802228 | ||
| 
						 | 
					de13f67ae5 | ||
| 
						 | 
					0256cc4830 | ||
| 
						 | 
					8422add614 | ||
| 
						 | 
					2114697d6f | ||
| 
						 | 
					412eaf599e | ||
| 
						 | 
					0691a35dab | ||
| 
						 | 
					f57191fd89 | ||
| 
						 | 
					14066b5c4d | 
@@ -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: {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "openmct",
 | 
			
		||||
  "version": "1.7.7-SNAPSHOT",
 | 
			
		||||
  "version": "1.7.5",
 | 
			
		||||
  "description": "The Open MCT core platform",
 | 
			
		||||
  "dependencies": {},
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
@@ -37,18 +37,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 +89,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 +100,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,41 +19,40 @@
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
define(function () {
 | 
			
		||||
 | 
			
		||||
import HyperlinkLayout from './HyperlinkLayout.vue';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
export default function HyperlinkProvider(openmct) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 {
 | 
			
		||||
        key: 'hyperlink.view',
 | 
			
		||||
        name: 'Hyperlink',
 | 
			
		||||
        cssClass: 'icon-chain-links',
 | 
			
		||||
        canView(domainObject) {
 | 
			
		||||
            return domainObject.type === 'hyperlink';
 | 
			
		||||
        mobile: function (agentService) {
 | 
			
		||||
            return agentService.isMobile();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        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();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										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,10 +20,33 @@
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import ViewLargeAction from './viewLargeAction.js';
 | 
			
		||||
 | 
			
		||||
export default function plugin() {
 | 
			
		||||
    return function install(openmct) {
 | 
			
		||||
        openmct.actions.register(new ViewLargeAction(openmct));
 | 
			
		||||
define([
 | 
			
		||||
    "./res/templates/deprecated-timeline-message.html"
 | 
			
		||||
], function (
 | 
			
		||||
    deprecatedTimelineMessage
 | 
			
		||||
) {
 | 
			
		||||
    return {
 | 
			
		||||
        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,32 +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 Agent from "../../utils/agent/Agent";
 | 
			
		||||
import DeviceClassifier from "./src/DeviceClassifier";
 | 
			
		||||
 | 
			
		||||
export default () => {
 | 
			
		||||
    return (openmct) => {
 | 
			
		||||
        openmct.on("start", () => {
 | 
			
		||||
            const agent = new Agent(window);
 | 
			
		||||
            DeviceClassifier(agent, window.document);
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
@@ -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,57 +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.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -138,7 +138,7 @@ export default class Condition extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateCriteria(criterionConfigurations) {
 | 
			
		||||
        this.destroyCriteria();
 | 
			
		||||
        this.destroyCriteria(true);
 | 
			
		||||
        this.createCriteria(criterionConfigurations);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -204,7 +204,7 @@ export default class Condition extends EventEmitter {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroyCriterion(id) {
 | 
			
		||||
    destroyCriterion(id, isRemoved) {
 | 
			
		||||
        let found = this.findCriterion(id);
 | 
			
		||||
        if (found) {
 | 
			
		||||
            let criterion = found.item;
 | 
			
		||||
@@ -216,7 +216,9 @@ export default class Condition extends EventEmitter {
 | 
			
		||||
            });
 | 
			
		||||
            criterion.destroy();
 | 
			
		||||
            this.criteria.splice(found.index, 1);
 | 
			
		||||
            this.updateDescription();
 | 
			
		||||
            if (isRemoved) {
 | 
			
		||||
                this.updateDescription();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
@@ -309,17 +311,17 @@ export default class Condition extends EventEmitter {
 | 
			
		||||
        return this.criteria;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroyCriteria() {
 | 
			
		||||
    destroyCriteria(isRemoved) {
 | 
			
		||||
        let success = true;
 | 
			
		||||
        //looping through the array backwards since destroyCriterion modifies the criteria array
 | 
			
		||||
        for (let i = this.criteria.length - 1; i >= 0; i--) {
 | 
			
		||||
            success = success && this.destroyCriterion(this.criteria[i].id);
 | 
			
		||||
            success = success && this.destroyCriterion(this.criteria[i].id, isRemoved);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        this.destroyCriteria();
 | 
			
		||||
    destroy(isRemoved) {
 | 
			
		||||
        this.destroyCriteria(isRemoved);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -227,7 +227,7 @@ export default class ConditionManager extends EventEmitter {
 | 
			
		||||
    removeCondition(id) {
 | 
			
		||||
        let index = this.conditions.findIndex(item => item.id === id);
 | 
			
		||||
        if (index > -1) {
 | 
			
		||||
            this.conditions[index].destroy();
 | 
			
		||||
            this.conditions[index].destroy(true);
 | 
			
		||||
            this.conditions.splice(index, 1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -437,7 +437,7 @@ export default class ConditionManager extends EventEmitter {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.conditions.forEach((condition) => {
 | 
			
		||||
            condition.destroy();
 | 
			
		||||
            condition.destroy(false);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
@@ -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');
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -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');
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
@@ -506,7 +507,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 +623,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 +927,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) || [];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,10 +25,7 @@ function ConfigStore() {
 | 
			
		||||
 | 
			
		||||
ConfigStore.prototype.deleteStore = function (id) {
 | 
			
		||||
    if (this.store[id]) {
 | 
			
		||||
        if (this.store[id].destroy) {
 | 
			
		||||
            this.store[id].destroy();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.store[id].destroy();
 | 
			
		||||
        delete this.store[id];
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -176,9 +176,7 @@ DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) {
 | 
			
		||||
    this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
 | 
			
		||||
    this.gl.uniform4fv(this.uColor, color);
 | 
			
		||||
    this.gl.uniform1i(this.uMarkerShape, shapeCode);
 | 
			
		||||
    if (points !== 0) {
 | 
			
		||||
        this.gl.drawArrays(drawType, 0, points);
 | 
			
		||||
    }
 | 
			
		||||
    this.gl.drawArrays(drawType, 0, points);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
DrawWebGL.prototype.clear = function () {
 | 
			
		||||
 
 | 
			
		||||
@@ -201,57 +201,15 @@ describe("the plugin", function () {
 | 
			
		||||
                        hints: {
 | 
			
		||||
                            range: 1
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        key: "yet-another-key",
 | 
			
		||||
                        format: "string",
 | 
			
		||||
                        hints: {
 | 
			
		||||
                            range: 2
 | 
			
		||||
                        }
 | 
			
		||||
                    }]
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
 | 
			
		||||
            const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
 | 
			
		||||
 | 
			
		||||
            let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple");
 | 
			
		||||
            expect(plotView).toBeDefined();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("does not provide a plot view if the telemetry is entirely non numeric", () => {
 | 
			
		||||
            const testTelemetryObject = {
 | 
			
		||||
                id: "test-object",
 | 
			
		||||
                type: "test-object",
 | 
			
		||||
                telemetry: {
 | 
			
		||||
                    values: [{
 | 
			
		||||
                        key: "some-key",
 | 
			
		||||
                        hints: {
 | 
			
		||||
                            domain: 1
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        key: "other-key",
 | 
			
		||||
                        format: "string",
 | 
			
		||||
                        hints: {
 | 
			
		||||
                            range: 1
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        key: "yet-another-key",
 | 
			
		||||
                        format: "string",
 | 
			
		||||
                        hints: {
 | 
			
		||||
                            range: 1
 | 
			
		||||
                        }
 | 
			
		||||
                    }]
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
 | 
			
		||||
            const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
 | 
			
		||||
 | 
			
		||||
            expect(plotView).toBeUndefined();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("provides an overlay plot view for objects with telemetry", () => {
 | 
			
		||||
            const testTelemetryObject = {
 | 
			
		||||
                id: "test-object",
 | 
			
		||||
@@ -325,6 +283,14 @@ describe("the plugin", function () {
 | 
			
		||||
                start: 0,
 | 
			
		||||
                end: 4
 | 
			
		||||
            });
 | 
			
		||||
            const getFunc = openmct.$injector.get;
 | 
			
		||||
            spyOn(openmct.$injector, "get")
 | 
			
		||||
                .withArgs("exportImageService").and.returnValue({
 | 
			
		||||
                    exportPNG: () => {},
 | 
			
		||||
                    exportJPG: () => {}
 | 
			
		||||
                })
 | 
			
		||||
                .and.callFake(getFunc);
 | 
			
		||||
 | 
			
		||||
            testTelemetryObject = {
 | 
			
		||||
                identifier: {
 | 
			
		||||
                    namespace: "",
 | 
			
		||||
@@ -357,7 +323,7 @@ describe("the plugin", function () {
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
 | 
			
		||||
            plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
 | 
			
		||||
            plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple");
 | 
			
		||||
            plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]);
 | 
			
		||||
            plotView.show(child, true);
 | 
			
		||||
 | 
			
		||||
@@ -605,7 +571,7 @@ describe("the plugin", function () {
 | 
			
		||||
            expect(legend.length).toBe(6);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        xit("Renders X-axis ticks for the telemetry object", () => {
 | 
			
		||||
        it("Renders X-axis ticks for the telemetry object", () => {
 | 
			
		||||
            let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
 | 
			
		||||
            expect(xAxisElement.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
@@ -715,15 +681,14 @@ describe("the plugin", function () {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("Adds a new point to the plot", (done) => {
 | 
			
		||||
            let originalLength = config.series.models[0].getSeriesData().length;
 | 
			
		||||
            let originalLength = config.series.models[0].data.length;
 | 
			
		||||
            config.series.models[0].add({
 | 
			
		||||
                utc: 2,
 | 
			
		||||
                'some-key': 1,
 | 
			
		||||
                'some-other-key': 2
 | 
			
		||||
            });
 | 
			
		||||
            Vue.nextTick(() => {
 | 
			
		||||
                const seriesData = config.series.models[0].getSeriesData();
 | 
			
		||||
                expect(seriesData.length).toEqual(originalLength + 1);
 | 
			
		||||
                expect(config.series.models[0].data.length).toEqual(originalLength + 1);
 | 
			
		||||
                done();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user