Compare commits

..

47 Commits

Author SHA1 Message Date
6526f3d685 Adds a function to the Object API 2021-09-02 15:58:53 -07:00
b1b4266ff3 Add automated security scanning to our repo (#4166) 2021-08-30 15:44:25 -07:00
42b0148f93 Add new icon for aggregate telemetry (#4163) 2021-08-30 11:19:47 -07:00
9461ad8edd Add new Ellipse drawing object in Display Layouts (#4153)
* Add new ellipse drawing object to Display Layouts
- Fix test and linting issues;

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

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

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

* handle edge case when section is null/undefined.

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

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

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

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

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

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

fixed issue where large imagery view opens only once.

Co-authored-by: Henry Hsu <hhsu0219@gmail.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2021-08-10 07:00:13 -07:00
fe928a1386 [Imagery] Click on image to get a large view #3582 (#4077)
fixed issue where large imagery view opens only once.
2021-08-04 12:57:48 -07:00
b329ed6ed5 Couch object provider performance improvement using SharedWorker (#3993)
* Use the window SharedWorker instead of the WorkerService
* Use relative asset path for Shared Workers
* Remove beforeunload listener on destroy
2021-07-30 15:23:02 -07:00
9b7a0d7e4c Reimplement hyperlink vue (#4062)
* added vue hyperlink plugin
* remove angular code. update target attribute
* Polishing on form styles
- Remove `display: flex` from `.l-shell__main-container` and
`.c-so-view__object-view` CSS - IMPORTANT: NEEDS REGRESSION TESTING!
- Improvements to `.c-hyperlink` CSS;
- Markup cleanups and simplification;
- Remove duped CSS in object-frame.scss, probably result of prior bad
past merge;
* Fixes for object-frame and preview.scss
* Refinement to make hyperlink button have same display behavior as
Condition Widget;
* refactor layout template. update tests
* remove legacy hyperlink
* Updating firefox launcher

Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
2021-07-30 13:49:31 -07:00
5c15e53abb Mct4039 (#4057)
Re-implements ImageExportService as ES6 class instead of Angular managed service.

Co-authored-by: John Hill <jchill2.spam@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-29 22:05:18 -07:00
f58b3881f2 Not everything fits into the two types of issue (#4064)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-29 17:17:51 -07:00
071a13b219 [Imagery] Click on image to get a large view (#3770)
* [Imagery] Click on image to get a large view #3582
* Created new viewLargeAction.
* Changes in view registry to add parent element property inside view object.
* Separate class for views and added missing changes for LadTableSet.
* Renamed callBack to onItemClicked.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-29 09:19:07 -07:00
ca66898e51 [Plugin] Remote Clock based off Telemetry (#3998)
* Remote-clock plugin
* Added a default clock class that can be extended by other classes
* Updated local clock to use the parent default clock class
* added a period check to make sure its been a certain amount of time before emitting
Co-authored-by: John Hill <jchill2.spam@gmail.com>
2021-07-28 16:46:08 -07:00
94c7b2343a Add Enhancement request template (#4063)
* Add Enhancement request template

* Link to github discussions

* Update config.yml
2021-07-27 15:02:30 -07:00
c397c336ab [Telemetry Tables] Observe for changes in table configuration (#4053) 2021-07-27 14:08:44 -07:00
eea23f2caf Fix typo in template (#4059) 2021-07-27 11:05:02 -07:00
6665641c02 Update issue templates (#4058)
Add a bug report directly to the project
2021-07-27 10:38:24 -07:00
c3ebf52dd2 Prepare for sprint 1.7.6 (#4052) 2021-07-26 14:04:26 -07:00
f8f2e7da9b Remove deprecated timeline bundle (#4048) 2021-07-23 13:51:32 -07:00
240f58b2d0 [Telemetry API] wrap limits function return in promise if needed (#4044) 2021-07-20 07:25:43 -07:00
7d3baee7b5 URL Params to hide tree and inspector (#3951)
* Add checks and hide panes accordingly, toggle hide params when toggling panes, add params on change event
* add tests

Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
Co-authored-by: John Hill <jchill2.spam@gmail.com>
2021-07-19 10:01:05 -07:00
1f5cb7ca42 Prevent default on click events for conductor delta buttons (#4022)
* Prevent default on click events for conductor delta buttons
2021-07-15 14:46:53 -07:00
4a7ebe326c Plot view policy fix (#3995)
* deny plot view for non-numeric telemetry

* revert plot type for backwards compatibility
2021-07-14 18:12:26 -07:00
10da314a4a Add resize event and disable pointer events on iframes on trigger of resize (#4016) 2021-07-14 17:32:45 -07:00
b3ceccd7fb temporarily skip test on chrome (#4021) 2021-07-14 15:58:44 -07:00
1bde4c9a0c Update all unit test packages, set node engine rules, new circleci workflow, pin to stable (#3957)
* Update all unit test packages, set node engine rules, new circleci workflow

Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-14 09:26:38 -07:00
4b85360446 1.7.4 stable master (#4015) 2021-07-13 13:59:37 -07:00
41b860a547 Fixes for Imagery Snapshotting #3963 (#4001)
* refactored compass structure and code.
* resize image wrapper
* center image properly
* Refactor imagery compass rose as SVG
* Suppress prev/next image arrows from Snapshot

Co-authored-by: Nikhil Mandlik <nikhil.k.mandlik@nasa.gov>
2021-07-09 18:03:11 -07:00
254b3db966 added mock values to compass for example imagery. (#3999) 2021-07-09 15:13:54 -07:00
203 changed files with 5839 additions and 4290 deletions

View File

@ -1,36 +1,93 @@
version: 2
jobs:
build:
version: 2.1
executors:
linux:
docker:
- image: circleci/node:13-browsers
environment:
CHROME_BIN: "/usr/bin/google-chrome"
steps:
- 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:
version: 2
- image: cimg/base:stable
orbs:
node: circleci/node@4.5.1
browser-tools: circleci/browser-tools@1.1.3
jobs:
test:
parameters:
node-version:
type: string
browser:
type: string
always-pass:
type: boolean
executor: linux
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/
workflows:
matrix-tests:
jobs:
- build
- test:
name: node10-chrome
node-version: lts/dubnium
browser: ChromeHeadless
always-pass: false
- test:
name: node12-firefoxESR-build-only
node-version: lts/erbium
browser: FirefoxESR
always-pass: true
- test:
name: node14-chrome-build-only
node-version: lts/fermium
browser: ChromeHeadless
always-pass: true
nightly:
jobs:
- test:
name: node10-chrome-nightly
node-version: lts/dubnium
browser: ChromeHeadless
always-pass: false
- test:
name: node12-firefoxESR-nightly
node-version: lts/erbium
browser: FirefoxESR
always-pass: false
- test:
name: node14-chrome-nightly
node-version: lts/fermium
browser: ChromeHeadless
always-pass: false
triggers:
- schedule:
cron: "0 0 * * *"
filters:
branches:
only:
- master

View File

@ -1,11 +1,12 @@
<!--- 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. -->
@ -35,7 +36,7 @@ about: File a Bug !
#### Environment
* Open MCT Version: <!--- date of build, version, or SHA -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yams? -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
* OS:
* Browser:

View File

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

View File

@ -0,0 +1,20 @@
---
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.

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

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

View File

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

1
.npmrc Normal file
View File

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

2
API.md
View File

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

View File

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

View File

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

View File

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

View File

@ -49,6 +49,10 @@ define([
];
const IMAGE_DELAY = 20000;
function getCompassValues(min, max) {
return min + Math.random() * (max - min);
}
function pointForTimestamp(timestamp, name) {
const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length];
const urlItems = url.split('/');
@ -59,6 +63,9 @@ define([
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
url,
sunOrientation: getCompassValues(0, 360),
cameraPan: getCompassValues(0, 360),
heading: getCompassValues(0, 360),
imageDownloadName
};
}

View File

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

View File

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

View File

@ -88,6 +88,7 @@
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"
@ -194,7 +195,6 @@
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
{indicator: true}
));
openmct.install(openmct.plugins.CodeWalkthrough);
openmct.start();
</script>
</html>

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "1.7.4",
"version": "1.7.8-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {
@ -34,20 +34,21 @@
"git-rev-sync": "^1.4.0",
"glob": ">= 3.0.0",
"html-loader": "^0.5.5",
"html2canvas": "^1.0.0-alpha.12",
"html2canvas": "^1.0.0-rc.7",
"imports-loader": "^0.8.0",
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.1.0",
"jasmine-core": "^3.7.1",
"jsdoc": "^3.3.2",
"karma": "5.1.1",
"karma": "6.3.4",
"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-firefox-launcher": "1.3.0",
"karma-junit-reporter": "2.0.1",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "3.3.1",
"karma-sourcemap-loader": "0.3.7",
"karma-jasmine": "4.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-webpack": "4.0.2",
"location-bar": "^3.0.1",
"lodash": "^4.17.12",
@ -89,6 +90,7 @@
"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",
@ -100,6 +102,9 @@
"type": "git",
"url": "https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=10.10.2 <16.0.0"
},
"author": "",
"license": "Apache-2.0",
"private": true

View File

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

View File

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

View File

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

View File

@ -0,0 +1,96 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import AgentService from "./AgentService";
const TEST_USER_AGENTS = {
DESKTOP:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36",
IPAD:
"Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
IPHONE:
"Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53"
};
describe("The AgentService", function () {
let testWindow;
let agentService;
beforeEach(function () {
testWindow = {
innerWidth: 640,
innerHeight: 480,
navigator: {
userAgent: TEST_USER_AGENTS.DESKTOP
}
};
});
it("recognizes desktop devices as non-mobile", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeFalsy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPhones", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeTruthy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPads", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeTruthy();
});
it("detects display orientation", function () {
agentService = new AgentService(testWindow);
testWindow.innerWidth = 1024;
testWindow.innerHeight = 400;
expect(agentService.isPortrait()).toBeFalsy();
expect(agentService.isLandscape()).toBeTruthy();
testWindow.innerWidth = 400;
testWindow.innerHeight = 1024;
expect(agentService.isPortrait()).toBeTruthy();
expect(agentService.isLandscape()).toBeFalsy();
});
it("detects touch support", function () {
testWindow.ontouchstart = null;
expect(new AgentService(testWindow).isTouch()).toBe(true);
delete testWindow.ontouchstart;
expect(new AgentService(testWindow).isTouch()).toBe(false);
});
it("allows for checking browser type", function () {
testWindow.navigator.userAgent = "Chromezilla Safarifox";
agentService = new AgentService(testWindow);
expect(agentService.isBrowser("Chrome")).toBe(true);
expect(agentService.isBrowser("Firefox")).toBe(false);
});
});

View File

@ -1,72 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['./DeviceMatchers'],
function (DeviceMatchers) {
/**
* Runs at application startup and adds a subset of the following
* CSS classes to the body of the document, depending on device
* attributes:
*
* * `mobile`: Phones or tablets.
* * `phone`: Phones specifically.
* * `tablet`: Tablets specifically.
* * `desktop`: Non-mobile devices.
* * `portrait`: Devices in a portrait-style orientation.
* * `landscape`: Devices in a landscape-style orientation.
* * `touch`: Device supports touch events.
*
* @param {platform/commonUI/mobile.AgentService} agentService
* the service used to examine the user agent
* @param $document Angular's jqLite-wrapped document element
* @constructor
*/
function MobileClassifier(agentService, $document) {
var body = $document.find('body');
Object.keys(DeviceMatchers).forEach(function (key, index, array) {
if (DeviceMatchers[key](agentService)) {
body.addClass(key);
}
});
if (agentService.isMobile()) {
var mediaQuery = window.matchMedia('(orientation: landscape)');
mediaQuery.addListener(function (event) {
if (event.matches) {
body.removeClass('portrait');
body.addClass('landscape');
} else {
body.removeClass('landscape');
body.addClass('portrait');
}
});
}
}
return MobileClassifier;
}
);

View File

@ -1,88 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['./DeviceMatchers'],
function (DeviceMatchers) {
/**
* The `mct-device` directive, when applied as an attribute,
* only includes the element when the device being used matches
* a set of characteristics required.
*
* Required characteristics are given as space-separated strings
* as the value to this attribute, e.g.:
*
* <span mct-device="mobile portrait">Hello world!</span>
*
* ...will only show Hello world! when viewed on a mobile device
* in the portrait orientation.
*
* Valid device characteristics to detect are:
*
* * `mobile`: Phones or tablets.
* * `phone`: Phones specifically.
* * `tablet`: Tablets specifically.
* * `desktop`: Non-mobile devices.
* * `portrait`: Devices in a portrait-style orientation.
* * `landscape`: Devices in a landscape-style orientation.
* * `touch`: Device supports touch events.
*
* @param {AgentService} agentService used to detect device type
* based on information about the user agent
*/
function MCTDevice(agentService) {
function deviceMatches(tokens) {
tokens = tokens || "";
return tokens.split(" ").every(function (token) {
var fn = DeviceMatchers[token];
return fn && fn(agentService);
});
}
function link(scope, element, attrs, ctrl, transclude) {
if (deviceMatches(attrs.mctDevice)) {
transclude(function (clone) {
element.replaceWith(clone);
});
}
}
return {
link: link,
// We are transcluding the whole element (like ng-if)
transclude: 'element',
// 1 more than ng-if
priority: 601,
// Also terminal, since element will be transcluded
terminal: true,
// Only apply as an attribute
restrict: "A"
};
}
return MCTDevice;
}
);

View File

@ -1,99 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../src/AgentService"],
function (AgentService) {
var TEST_USER_AGENTS = {
DESKTOP: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36",
IPAD: "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
IPHONE: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53"
};
describe("The AgentService", function () {
var testWindow, agentService;
beforeEach(function () {
testWindow = {
innerWidth: 640,
innerHeight: 480,
navigator: {
userAgent: TEST_USER_AGENTS.DESKTOP
}
};
});
it("recognizes desktop devices as non-mobile", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeFalsy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPhones", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeTruthy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPads", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeTruthy();
});
it("detects display orientation", function () {
agentService = new AgentService(testWindow);
testWindow.innerWidth = 1024;
testWindow.innerHeight = 400;
expect(agentService.isPortrait()).toBeFalsy();
expect(agentService.isLandscape()).toBeTruthy();
testWindow.innerWidth = 400;
testWindow.innerHeight = 1024;
expect(agentService.isPortrait()).toBeTruthy();
expect(agentService.isLandscape()).toBeFalsy();
});
it("detects touch support", function () {
testWindow.ontouchstart = null;
expect(new AgentService(testWindow).isTouch())
.toBe(true);
delete testWindow.ontouchstart;
expect(new AgentService(testWindow).isTouch())
.toBe(false);
});
it("allows for checking browser type", function () {
testWindow.navigator.userAgent = "Chromezilla Safarifox";
agentService = new AgentService(testWindow);
expect(agentService.isBrowser("Chrome")).toBe(true);
expect(agentService.isBrowser("Firefox")).toBe(false);
});
});
}
);

View File

@ -1,109 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../src/DeviceClassifier", "../src/DeviceMatchers"],
function (DeviceClassifier, DeviceMatchers) {
var AGENT_SERVICE_METHODS = [
'isMobile',
'isPhone',
'isTablet',
'isPortrait',
'isLandscape',
'isTouch'
],
TEST_PERMUTATIONS = [
['isMobile', 'isPhone', 'isTouch', 'isPortrait'],
['isMobile', 'isPhone', 'isTouch', 'isLandscape'],
['isMobile', 'isTablet', 'isTouch', 'isPortrait'],
['isMobile', 'isTablet', 'isTouch', 'isLandscape'],
['isTouch'],
[]
];
describe("DeviceClassifier", function () {
var mockAgentService,
mockDocument,
mockBody;
beforeEach(function () {
mockAgentService = jasmine.createSpyObj(
'agentService',
AGENT_SERVICE_METHODS
);
mockDocument = jasmine.createSpyObj(
'$document',
['find']
);
mockBody = jasmine.createSpyObj(
'body',
['addClass']
);
mockDocument.find.and.callFake(function (sel) {
return sel === 'body' && mockBody;
});
AGENT_SERVICE_METHODS.forEach(function (m) {
mockAgentService[m].and.returnValue(false);
});
});
TEST_PERMUTATIONS.forEach(function (trueMethods) {
var summary = trueMethods.length === 0
? "device has no detected characteristics"
: "device " + (trueMethods.join(", "));
describe("when " + summary, function () {
var classifier; // eslint-disable-line
beforeEach(function () {
trueMethods.forEach(function (m) {
mockAgentService[m].and.returnValue(true);
});
classifier = new DeviceClassifier(
mockAgentService,
mockDocument
);
});
it("adds classes for matching, detected characteristics", function () {
Object.keys(DeviceMatchers).filter(function (m) {
return DeviceMatchers[m](mockAgentService);
}).forEach(function (key) {
expect(mockBody.addClass)
.toHaveBeenCalledWith(key);
});
});
it("does not add classes for non-matching characteristics", function () {
Object.keys(DeviceMatchers).filter(function (m) {
return !DeviceMatchers[m](mockAgentService);
}).forEach(function (key) {
expect(mockBody.addClass)
.not.toHaveBeenCalledWith(key);
});
});
});
});
});
}
);

View File

@ -1,78 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../src/DeviceMatchers"],
function (DeviceMatchers) {
describe("DeviceMatchers", function () {
var mockAgentService;
beforeEach(function () {
mockAgentService = jasmine.createSpyObj(
'agentService',
[
'isMobile',
'isPhone',
'isTablet',
'isPortrait',
'isLandscape',
'isTouch'
]
);
});
it("detects when a device is a desktop device", function () {
mockAgentService.isMobile.and.returnValue(false);
expect(DeviceMatchers.desktop(mockAgentService))
.toBe(true);
mockAgentService.isMobile.and.returnValue(true);
expect(DeviceMatchers.desktop(mockAgentService))
.toBe(false);
});
function method(deviceType) {
return "is" + deviceType[0].toUpperCase() + deviceType.slice(1);
}
[
"mobile",
"phone",
"tablet",
"landscape",
"portrait",
"landscape",
"touch"
].forEach(function (deviceType) {
it("detects when a device is a " + deviceType + " device", function () {
mockAgentService[method(deviceType)].and.returnValue(true);
expect(DeviceMatchers[deviceType](mockAgentService))
.toBe(true);
mockAgentService[method(deviceType)].and.returnValue(false);
expect(DeviceMatchers[deviceType](mockAgentService))
.toBe(false);
});
});
});
}
);

View File

@ -1,168 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['../src/MCTDevice'],
function (MCTDevice) {
var JQLITE_METHODS = ['replaceWith'];
describe("The mct-device directive", function () {
var mockAgentService,
mockTransclude,
mockElement,
mockClone,
testAttrs,
directive;
function link() {
directive.link(null, mockElement, testAttrs, null, mockTransclude);
}
beforeEach(function () {
mockAgentService = jasmine.createSpyObj(
"agentService",
["isMobile", "isPhone", "isTablet", "isPortrait", "isLandscape"]
);
mockTransclude = jasmine.createSpy("$transclude");
mockElement = jasmine.createSpyObj(name, JQLITE_METHODS);
mockClone = jasmine.createSpyObj(name, JQLITE_METHODS);
mockTransclude.and.callFake(function (fn) {
fn(mockClone);
});
// Look desktop-like by default
mockAgentService.isLandscape.and.returnValue(true);
testAttrs = {};
directive = new MCTDevice(mockAgentService);
});
function expectInclusion() {
expect(mockElement.replaceWith)
.toHaveBeenCalledWith(mockClone);
}
function expectExclusion() {
expect(mockElement.replaceWith).not.toHaveBeenCalled();
}
it("is applicable at the attribute level", function () {
expect(directive.restrict).toEqual("A");
});
it("transcludes at the element level", function () {
expect(directive.transclude).toEqual('element');
});
it("has a greater priority number than ng-if", function () {
expect(directive.priority > 600).toBeTruthy();
});
it("restricts element inclusion for mobile devices", function () {
testAttrs.mctDevice = "mobile";
link();
expectExclusion();
mockAgentService.isMobile.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for tablet devices", function () {
testAttrs.mctDevice = "tablet";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isTablet.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for phone devices", function () {
testAttrs.mctDevice = "phone";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isPhone.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for desktop devices", function () {
testAttrs.mctDevice = "desktop";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isMobile.and.returnValue(false);
link();
expectInclusion();
});
it("restricts element inclusion for portrait orientation", function () {
testAttrs.mctDevice = "portrait";
link();
expectExclusion();
mockAgentService.isPortrait.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for landscape orientation", function () {
testAttrs.mctDevice = "landscape";
mockAgentService.isLandscape.and.returnValue(false);
mockAgentService.isPortrait.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isLandscape.and.returnValue(true);
link();
expectInclusion();
});
it("allows multiple device characteristics to be requested", function () {
// Won't try to test every permutation here, just
// make sure the multi-characteristic feature has support.
testAttrs.mctDevice = "portrait mobile";
link();
// Neither portrait nor mobile, not called
expectExclusion();
mockAgentService.isPortrait.and.returnValue(true);
link();
// Was portrait, but not mobile, so no
expectExclusion();
mockAgentService.isMobile.and.returnValue(true);
link();
expectInclusion();
});
});
}
);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +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.
-->
<a class="c-hyperlink u-links" ng-controller="HyperlinkController as hyperlink" href="{{domainObject.getModel().url}}"
ng-attr-target="{{hyperlink.openNewTab() ? '_blank' : undefined}}"
ng-class="{
'c-hyperlink--button u-fills-container' : hyperlink.isButton(),
'c-hyperlink--link' : !hyperlink.isButton() }">
<span class="c-hyperlink__label">{{domainObject.getModel().displayText}}</span>
</a>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -122,6 +122,7 @@ define([
}
};
this.destroy = this.destroy.bind(this);
/**
* Tracks current selection state of the application.
* @private
@ -262,7 +263,7 @@ define([
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin());
this.install(LicensesPlugin.default());
@ -283,8 +284,10 @@ 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);
@ -434,6 +437,8 @@ define([
Browse(this);
}
window.addEventListener('beforeunload', this.destroy);
this.router.start();
this.emit('start');
}.bind(this));
@ -457,6 +462,7 @@ define([
};
MCT.prototype.destroy = function () {
window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy');
this.router.destroy();
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -129,9 +129,7 @@ class MutableDomainObject {
mutable.$observe('$_synchronize_model', (updatedObject) => {
let clone = JSON.parse(JSON.stringify(updatedObject));
let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject));
deleted.forEach((propertyName) => delete mutable[propertyName]);
Object.assign(mutable, clone);
utils.refresh(mutable, clone);
});
return mutable;

View File

@ -389,6 +389,23 @@ ObjectAPI.prototype.mutate = function (domainObject, path, value) {
}
};
/**
* Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
* @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
* @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
*/
ObjectAPI.prototype.refresh = async function (domainObject) {
const refreshedObject = await this.get(domainObject.identifier);
if (domainObject.isMutable) {
domainObject.$refresh(refreshedObject);
} else {
utils.refresh(domainObject, refreshedObject);
}
return domainObject;
};
/**
* @private
*/

View File

@ -223,6 +223,28 @@ describe("The Object API", () => {
expect(testObject.name).toBe(MUTATED_NAME);
});
it('Provides a way of refreshing an object from the persistence store', () => {
const modifiedTestObject = JSON.parse(JSON.stringify(testObject));
const OTHER_ATTRIBUTE_VALUE = 'Modified value';
const NEW_ATTRIBUTE_VALUE = 'A new attribute';
modifiedTestObject.otherAttribute = OTHER_ATTRIBUTE_VALUE;
modifiedTestObject.newAttribute = NEW_ATTRIBUTE_VALUE;
delete modifiedTestObject.objectAttribute;
spyOn(objectAPI, 'get');
objectAPI.get.and.returnValue(Promise.resolve(modifiedTestObject));
expect(objectAPI.get).not.toHaveBeenCalled();
return objectAPI.refresh(testObject).then(() => {
expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier);
expect(testObject.otherAttribute).toEqual(OTHER_ATTRIBUTE_VALUE);
expect(testObject.newAttribute).toEqual(NEW_ATTRIBUTE_VALUE);
expect(testObject.objectAttribute).not.toBeDefined();
});
});
describe ('uses a MutableDomainObject', () => {
it('and retains properties of original object ', function () {
expect(hasOwnProperty(mutable, 'identifier')).toBe(true);

View File

@ -165,12 +165,19 @@ define([
return identifierEquals(a.identifier, b.identifier);
}
function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
}
return {
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString,
equals: objectEquals,
identifierEquals: identifierEquals
identifierEquals: identifierEquals,
refresh: refresh
};
});

View File

@ -60,7 +60,7 @@ class OverlayAPI {
* A description of option properties that can be passed into the overlay
* @typedef options
* @property {object} element DOMElement that is to be inserted/shown on the overlay
* @property {string} size prefered size of the overlay (large, small, fit)
* @property {string} size preferred size of the overlay (large, small, fit)
* @property {array} buttons optional button objects with label and callback properties
* @property {function} onDestroy callback to be called when overlay is destroyed
* @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,388 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import _ from 'lodash';
import EventEmitter from 'EventEmitter';
const ERRORS = {
TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.',
LOADED: 'Telemetry Collection has already been loaded.'
};
/** Class representing a Telemetry Collection. */
export class TelemetryCollection extends EventEmitter {
/**
* Creates a Telemetry Collection
*
* @param {object} openmct - Openm MCT
* @param {object} domainObject - Domain Object to user for telemetry collection
* @param {object} options - Any options passed in for request/subscribe
*/
constructor(openmct, domainObject, options) {
super();
this.loaded = false;
this.openmct = openmct;
this.domainObject = domainObject;
this.boundedTelemetry = [];
this.futureBuffer = [];
this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined;
this.historicalProvider = undefined;
this.options = options;
this.pageState = undefined;
this.lastBounds = undefined;
this.requestAbort = undefined;
}
/**
* This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/
load() {
if (this.loaded) {
this._error(ERRORS.LOADED);
}
this._timeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds();
this._watchBounds();
this._watchTimeSystem();
this._initiateHistoricalRequests();
this._initiateSubscriptionTelemetry();
this.loaded = true;
}
/**
* can/should be called by the requester of the telemetry collection
* to remove any listeners
*/
destroy() {
if (this.requestAbort) {
this.requestAbort.abort();
}
this._unwatchBounds();
this._unwatchTimeSystem();
if (this.unsubscribe) {
this.unsubscribe();
}
this.removeAllListeners();
}
/**
* This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/
getAll() {
return this.boundedTelemetry;
}
/**
* Sets up the telemetry collection for historical requests,
* this uses the "standardizeRequestOptions" from Telemetry API
* @private
*/
_initiateHistoricalRequests() {
this.openmct.telemetry.standardizeRequestOptions(this.options);
this.historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, this.options);
this._requestHistoricalTelemetry();
}
/**
* If a historical provider exists, then historical requests will be made
* @private
*/
async _requestHistoricalTelemetry() {
if (!this.historicalProvider) {
return;
}
let historicalData;
try {
this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal;
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
this.requestAbort = undefined;
} catch (error) {
console.error('Error requesting telemetry data...');
this.requestAbort = undefined;
this._error(error);
}
this._processNewTelemetry(historicalData);
}
/**
* This uses the built in subscription function from Telemetry API
* @private
*/
_initiateSubscriptionTelemetry() {
if (this.unsubscribe) {
this.unsubscribe();
}
this.unsubscribe = this.openmct.telemetry
.subscribe(
this.domainObject,
datum => this._processNewTelemetry(datum),
this.options
);
}
/**
* Filter any new telemetry (add/page, historical, subscription) based on
* time bounds and dupes
*
* @param {(Object|Object[])} telemetryData - telemetry data object or
* array of telemetry data objects
* @private
*/
_processNewTelemetry(telemetryData) {
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
let afterEndOfBounds;
let added = [];
for (let datum of data) {
parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start;
afterEndOfBounds = parsedValue > this.lastBounds.end;
if (!afterEndOfBounds && !beforeStartOfBounds) {
let isDuplicate = false;
let startIndex = this._sortedIndex(datum);
let endIndex = undefined;
// dupe check
if (startIndex !== this.boundedTelemetry.length) {
endIndex = _.sortedLastIndexBy(
this.boundedTelemetry,
datum,
boundedDatum => this.parseTime(boundedDatum)
);
if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
}
}
if (!isDuplicate) {
let index = endIndex || startIndex;
this.boundedTelemetry.splice(index, 0, datum);
added.push(datum);
}
} else if (afterEndOfBounds) {
this.futureBuffer.push(datum);
}
}
if (added.length) {
this.emit('add', added);
}
}
/**
* Finds the correct insertion point for the given telemetry datum.
* Leverages lodash's `sortedIndexBy` function which implements a binary search.
* @private
*/
_sortedIndex(datum) {
if (this.boundedTelemetry.length === 0) {
return 0;
}
let parsedValue = this.parseTime(datum);
let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]);
if (parsedValue > lastValue || parsedValue === lastValue) {
return this.boundedTelemetry.length;
} else {
return _.sortedIndexBy(
this.boundedTelemetry,
datum,
boundedDatum => this.parseTime(boundedDatum)
);
}
}
/**
* when the start time, end time, or both have been updated.
* data could be added OR removed here we update the current
* bounded telemetry
*
* @param {TimeConductorBounds} bounds The newly updated bounds
* @param {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
* @private
*/
_bounds(bounds, isTick) {
let startChanged = this.lastBounds.start !== bounds.start;
let endChanged = this.lastBounds.end !== bounds.end;
this.lastBounds = bounds;
if (isTick) {
// need to check futureBuffer and need to check
// if anything has fallen out of bounds
let startIndex = 0;
let endIndex = 0;
let discarded = [];
let added = [];
let testDatum = {};
if (startChanged) {
testDatum[this.timeKey] = bounds.start;
// Calculate the new index of the first item within the bounds
startIndex = _.sortedIndexBy(
this.boundedTelemetry,
testDatum,
datum => this.parseTime(datum)
);
discarded = this.boundedTelemetry.splice(0, startIndex);
}
if (endChanged) {
testDatum[this.timeKey] = bounds.end;
// Calculate the new index of the last item in bounds
endIndex = _.sortedLastIndexBy(
this.futureBuffer,
testDatum,
datum => this.parseTime(datum)
);
added = this.futureBuffer.splice(0, endIndex);
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
}
if (discarded.length > 0) {
this.emit('remove', discarded);
}
if (added.length > 0) {
this.emit('add', added);
}
} else {
// user bounds change, reset
this._reset();
}
}
/**
* whenever the time system is updated need to update related values in
* the Telemetry Collection and reset the telemetry collection
*
* @param {TimeSystem} timeSystem - the value of the currently applied
* Time System
* @private
*/
_timeSystem(timeSystem) {
let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key);
if (domain === undefined) {
this._error(ERRORS.TIMESYSTEM_KEY);
}
// timeKey is used to create a dummy datum used for sorting
this.timeKey = domain.source; // this defaults to key if no source is set
let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
this.parseTime = (datum) => {
return valueFormatter.parse(datum);
};
this._reset();
}
/**
* Reset the telemetry data of the collection, and re-request
* historical telemetry
* @private
*
* @todo handle subscriptions more granually
*/
_reset() {
this.boundedTelemetry = [];
this.futureBuffer = [];
this._requestHistoricalTelemetry();
}
/**
* adds the _bounds callback to the 'bounds' timeAPI listener
* @private
*/
_watchBounds() {
this.openmct.time.on('bounds', this._bounds, this);
}
/**
* removes the _bounds callback from the 'bounds' timeAPI listener
* @private
*/
_unwatchBounds() {
this.openmct.time.off('bounds', this._bounds, this);
}
/**
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener
* @private
*/
_watchTimeSystem() {
this.openmct.time.on('timeSystem', this._timeSystem, this);
}
/**
* removes the _timeSystem callback from the 'timeSystem' timeAPI listener
* @private
*/
_unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this._timeSystem, this);
}
/**
* will throw a new Error, for passed in message
* @param {string} message Message describing the error
* @private
*/
_error(message) {
throw new Error(message);
}
}

View File

@ -0,0 +1,188 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Class defining an image exporter for JPG/PNG output.
* Originally created by hudsonfoo on 09/02/16
*/
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
return filename.replace(regex, '_');
}
import {saveAs} from 'file-saver/FileSaver';
import html2canvas from 'html2canvas';
import uuid from 'uuid';
class ImageExporter {
constructor(openmct) {
this.openmct = openmct;
}
/**
* Converts an HTML element into a PNG or JPG Blob.
* @private
* @param {node} element that will be converted to an image
* @param {object} options Image options.
* @returns {promise}
*/
renderElement(element, { imageType, className, thumbnailSize }) {
const self = this;
const overlays = this.openmct.overlays;
const dialog = overlays.dialog({
iconClass: 'info',
message: 'Caputuring an image',
buttons: [
{
label: 'Cancel',
emphasis: true,
callback: function () {
dialog.dismiss();
}
}
]
});
let mimeType = 'image/png';
if (imageType === 'jpg') {
mimeType = 'image/jpeg';
}
let exportId = undefined;
let oldId = undefined;
if (className) {
const newUUID = uuid();
exportId = `$export-element-${newUUID}`;
oldId = element.id;
element.id = exportId;
}
return html2canvas(element, {
useCORS: true,
allowTaint: true,
logging: false,
onclone: function (document) {
if (className) {
const clonedElement = document.getElementById(exportId);
clonedElement.classList.add(className);
}
element.id = oldId;
},
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) {
dialog.dismiss();
return new Promise(function (resolve, reject) {
if (thumbnailSize) {
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
return canvas.toBlob(blob => resolve({
blob,
thumbnail
}), mimeType);
}
return canvas.toBlob(blob => resolve({ blob }), mimeType);
});
}, function (error) {
console.log('error capturing image', error);
dialog.dismiss();
const errorDialog = overlays.dialog({
iconClass: 'error',
message: 'Image was not captured successfully!',
buttons: [
{
label: "OK",
emphasis: true,
callback: function () {
errorDialog.dismiss();
}
}
]
});
});
}
getThumbnail(canvas, mimeType, size) {
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.setAttribute('width', size.width);
thumbnailCanvas.setAttribute('height', size.height);
const ctx = thumbnailCanvas.getContext('2d');
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
return thumbnailCanvas.toDataURL(mimeType);
}
/**
* Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
async exportJPG(element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
const img = await this.renderElement(element, {
imageType: 'jpg',
className
});
saveAs(img.blob, processedFilename);
}
/**
* Takes a screenshot of a DOM node and exports to PNG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
async exportPNG(element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
const img = await this.renderElement(element, {
imageType: 'png',
className
});
saveAs(img.blob, processedFilename);
}
/**
* Takes a screenshot of a DOM node in PNG format.
* @param {node} element to be exported
* @param {string} filename the exported image
* @returns {promise}
*/
exportPNGtoSRC(element, options) {
return this.renderElement(element, {
imageType: 'png',
...options
});
}
}
export default ImageExporter;

View File

@ -0,0 +1,58 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageExporter from './ImageExporter';
import { createOpenMct, resetApplicationState } from '../utils/testing';
describe('The Image Exporter', () => {
let openmct;
let imageExporter;
beforeEach(() => {
openmct = createOpenMct();
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("basic instatation", () => {
it("can be instatiated", () => {
imageExporter = new ImageExporter(openmct);
expect(imageExporter).not.toEqual(null);
});
it("can render an element to a blob", async () => {
const mockHeadElement = document.createElement("h1");
const mockTextNode = document.createTextNode('foo bar');
mockHeadElement.appendChild(mockTextNode);
document.body.appendChild(mockHeadElement);
imageExporter = new ImageExporter(openmct);
const returnedBlob = await imageExporter.renderElement(document.body, {
imageType: 'png'
});
expect(returnedBlob).not.toEqual(null);
expect(returnedBlob.blob).not.toEqual(null);
expect(returnedBlob.blob).toBeInstanceOf(Blob);
});
});
});

View File

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

View File

@ -0,0 +1,32 @@
/*****************************************************************************
* 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);
});
};
};

View File

@ -0,0 +1,72 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Runs at application startup and adds a subset of the following
* CSS classes to the body of the document, depending on device
* attributes:
*
* * `mobile`: Phones or tablets.
* * `phone`: Phones specifically.
* * `tablet`: Tablets specifically.
* * `desktop`: Non-mobile devices.
* * `portrait`: Devices in a portrait-style orientation.
* * `landscape`: Devices in a landscape-style orientation.
* * `touch`: Device supports touch events.
*
* @param {utils/agent/Agent} agent
* the service used to examine the user agent
* @param document the HTML DOM document object
* @constructor
*/
import DeviceMatchers from "./DeviceMatchers";
export default (agent, document) => {
const body = document.body;
Object.keys(DeviceMatchers).forEach((key, index, array) => {
if (DeviceMatchers[key](agent)) {
body.classList.add(key);
}
});
if (agent.isMobile()) {
const mediaQuery = window.matchMedia("(orientation: landscape)");
function eventHandler(event) {
console.log("changed");
if (event.matches) {
body.classList.remove("portrait");
body.classList.add("landscape");
} else {
body.classList.remove("landscape");
body.classList.add("portrait");
}
}
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener(`change`, eventHandler);
} else {
// Deprecated 'MediaQueryList' API, <Safari 14, IE, <Edge 16
mediaQuery.addListener(eventHandler);
}
}
};

View File

@ -0,0 +1,105 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DeviceClassifier from "./DeviceClassifier";
import DeviceMatchers from "./DeviceMatchers";
const AGENT_METHODS = [
"isMobile",
"isPhone",
"isTablet",
"isPortrait",
"isLandscape",
"isTouch"
];
const TEST_PERMUTATIONS = [
["isMobile", "isPhone", "isTouch", "isPortrait"],
["isMobile", "isPhone", "isTouch", "isLandscape"],
["isMobile", "isTablet", "isTouch", "isPortrait"],
["isMobile", "isTablet", "isTouch", "isLandscape"],
["isTouch"],
[]
];
describe("DeviceClassifier", function () {
let mockAgent;
let mockDocument;
let mockClassList;
beforeEach(function () {
mockAgent = jasmine.createSpyObj(
"agent",
AGENT_METHODS
);
mockClassList = jasmine.createSpyObj("classList", ["add"]);
mockDocument = jasmine.createSpyObj(
"document",
{},
{ body: { classList: mockClassList } }
);
AGENT_METHODS.forEach(function (m) {
mockAgent[m].and.returnValue(false);
});
});
TEST_PERMUTATIONS.forEach(function (trueMethods) {
const summary =
trueMethods.length === 0
? "device has no detected characteristics"
: "device " + trueMethods.join(", ");
describe("when " + summary, function () {
beforeEach(function () {
trueMethods.forEach(function (m) {
mockAgent[m].and.returnValue(true);
});
// eslint-disable-next-line no-new
DeviceClassifier(mockAgent, mockDocument);
});
it("adds classes for matching, detected characteristics", function () {
Object.keys(DeviceMatchers)
.filter(function (m) {
return DeviceMatchers[m](mockAgent);
})
.forEach(function (key) {
expect(mockDocument.body.classList.add).toHaveBeenCalledWith(key);
});
});
it("does not add classes for non-matching characteristics", function () {
Object.keys(DeviceMatchers)
.filter(function (m) {
return !DeviceMatchers[m](mockAgent);
})
.forEach(function (key) {
expect(mockDocument.body.classList.add).not.toHaveBeenCalledWith(
key
);
});
});
});
});
});

View File

@ -0,0 +1,57 @@
/*****************************************************************************
* 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();
}
};

View File

@ -0,0 +1,65 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DeviceMatchers from "./DeviceMatchers";
describe("DeviceMatchers", function () {
let mockAgent;
beforeEach(function () {
mockAgent = jasmine.createSpyObj("agent", [
"isMobile",
"isPhone",
"isTablet",
"isPortrait",
"isLandscape",
"isTouch"
]);
});
it("detects when a device is a desktop device", function () {
mockAgent.isMobile.and.returnValue(false);
expect(DeviceMatchers.desktop(mockAgent)).toBe(true);
mockAgent.isMobile.and.returnValue(true);
expect(DeviceMatchers.desktop(mockAgent)).toBe(false);
});
function method(deviceType) {
return "is" + deviceType[0].toUpperCase() + deviceType.slice(1);
}
[
"mobile",
"phone",
"tablet",
"landscape",
"portrait",
"landscape",
"touch"
].forEach(function (deviceType) {
it("detects when a device is a " + deviceType + " device", function () {
mockAgent[method(deviceType)].and.returnValue(true);
expect(DeviceMatchers[deviceType](mockAgent)).toBe(true);
mockAgent[method(deviceType)].and.returnValue(false);
expect(DeviceMatchers[deviceType](mockAgent)).toBe(false);
});
});
});

View File

@ -19,8 +19,8 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import LadTableSet from './components/LadTableSet.vue';
import Vue from 'vue';
import LadTableSetView from './LadTableSetView';
export default function LADTableSetViewProvider(openmct) {
return {
@ -34,32 +34,7 @@ export default function LADTableSetViewProvider(openmct) {
return domainObject.type === 'LadTableSet';
},
view: function (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;
}
};
return new LadTableSetView(openmct, domainObject, objectPath);
},
priority: function () {
return 1;

View File

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

View File

@ -19,50 +19,30 @@
* 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';
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;
import LADTableView from './LADTableView';
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;
}
};
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,71 +0,0 @@
import Vue from 'Vue';
export default function install(openmct) {
openmct.objectViews.addProvider({
name: "Latest Data Table",
key: "latest-table",
cssClass: "icon-packet",
description: "A tabular view of telemetry contents.",
canView: function () {
return true;
},
view: function (domainObject) {
let unsubscribe;
return {
show: function (element) {
//element.innerText = 'Hello World!';
let telemetryMetadata = openmct.telemetry.getMetadata(domainObject).values();
let tableEl = document.createElement('table');
let tableHeader = document.createElement('thead');
let tableHeaderRow = document.createElement('tr');
let tableBody = document.createElement('tbody');
element.appendChild(tableEl);
tableHeader.appendChild(tableHeaderRow);
tableEl.appendChild(tableHeader);
tableEl.appendChild(tableBody);
telemetryMetadata.forEach(metadatum => {
let tableHeader = document.createElement('td');
tableHeader.innerText = metadatum.name;
tableHeaderRow.appendChild(tableHeader);
});
openmct.time.on('bounds', (newBounds) => {
tableBody.innerHTML = '';
requestTelemetry(newBounds);
});
requestTelemetry();
// unsubscribe = openmct.telemetry.subscribe(domainObject, (datum) => {
// addRow(datum);
// });
function addRow(telemetryDatum) {
let dataRow = document.createElement('tr');
telemetryMetadata.forEach(metadatum => {
let tableCell = document.createElement('td');
let formatter = openmct.telemetry.getValueFormatter(metadatum);
tableCell.innerText = formatter.format(telemetryDatum[metadatum.key]);
dataRow.appendChild(tableCell);
tableBody.appendChild(dataRow);
});
}
function requestTelemetry(bounds) {
openmct.telemetry.request(domainObject, {bounds}).then(arrayOfTelemetry => {
arrayOfTelemetry.forEach(addRow);
});
}
},
destroy: function (element) {
unsubscribe();
}
};
}
});
}

View File

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

View File

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

View File

@ -141,6 +141,7 @@ const NON_STYLEABLE_CONTAINER_TYPES = [
const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [
'line-view',
'box-view',
'ellipse-view',
'image-view'
];
@ -321,7 +322,7 @@ export default {
if (item) {
const type = this.openmct.types.get(item.type);
if (type && type.definition) {
creatable = (type.definition.creatable === true);
creatable = (type.definition.creatable !== undefined && (type.definition.creatable === 'true' || type.definition.creatable === true));
}
}

View File

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

View File

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

View File

@ -20,71 +20,78 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./components/AlphanumericFormatView.vue',
'vue'
], function (AlphanumericFormatView, Vue) {
import AlphanumericFormat from './components/AlphanumericFormat.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;
import Vue from 'vue';
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 selection.every(isTelemetryObject);
},
view: function (domainObject, objectPath) {
let component;
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;
}
};
class AlphanumericFormatView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
return AlphanumericFormatViewProvider;
});
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 this.component.$refs.alphanumericFormat.getViewContext();
}
destroy() {
this.component.$destroy();
this.component = undefined;
}
}
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 selection.every(isTelemetryObject);
},
view: function (domainObject, objectPath) {
return new AlphanumericFormatView(openmct, domainObject, objectPath);
},
priority: function () {
return 1;
}
};
}

View File

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

View File

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

View File

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

View File

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

View File

@ -56,6 +56,7 @@
:index="index"
:multi-select="selectedLayoutItems.length > 1"
:is-editing="isEditing"
@contextClick="updateViewContext"
@move="move"
@endMove="endMove"
@endLineResize="endLineResize"
@ -75,6 +76,7 @@ import uuid from 'uuid';
import SubobjectView from './SubobjectView.vue';
import TelemetryView from './TelemetryView.vue';
import BoxView from './BoxView.vue';
import EllipseView from './EllipseView.vue';
import TextView from './TextView.vue';
import LineView from './LineView.vue';
import ImageView from './ImageView.vue';
@ -111,6 +113,7 @@ const ITEM_TYPE_VIEW_MAP = {
'subobject-view': SubobjectView,
'telemetry-view': TelemetryView,
'box-view': BoxView,
'ellipse-view': EllipseView,
'line-view': LineView,
'text-view': TextView,
'image-view': ImageView
@ -140,7 +143,7 @@ function getItemDefinition(itemType, ...options) {
export default {
components: components,
inject: ['openmct', 'options', 'objectPath'],
inject: ['openmct', 'objectPath', 'options', 'objectUtils', 'currentView'],
props: {
domainObject: {
type: Object,
@ -155,7 +158,8 @@ export default {
return {
initSelectIndex: undefined,
selection: [],
showGrid: true
showGrid: true,
viewContext: {}
};
},
computed: {
@ -819,6 +823,12 @@ export default {
},
toggleGrid() {
this.showGrid = !this.showGrid;
},
updateViewContext(viewContext) {
this.viewContext.row = viewContext;
},
getViewContext() {
return this.viewContext;
}
}
};

View File

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

View File

@ -0,0 +1,122 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<layout-frame
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
<div
class="c-ellipse-view u-style-receiver js-style-receiver"
:class="[styleClass]"
:style="style"
></div>
</layout-frame>
</template>
<script>
import LayoutFrame from './LayoutFrame.vue';
import conditionalStylesMixin from '../mixins/objectStyles-mixin';
export default {
makeDefinition() {
return {
fill: '#666666',
stroke: '',
x: 1,
y: 1,
width: 10,
height: 10
};
},
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
inject: ['openmct'],
props: {
item: {
type: Object,
required: true
},
gridSize: {
type: Array,
required: true,
validator: (arr) => arr && arr.length === 2
&& arr.every(el => typeof el === 'number')
},
index: {
type: Number,
required: true
},
initSelect: Boolean,
isEditing: {
type: Boolean,
required: true
}
},
computed: {
style() {
if (this.itemStyle) {
return this.itemStyle;
} else {
return {
backgroundColor: this.item.fill,
border: this.item.stroke ? '1px solid ' + this.item.stroke : ''
};
}
}
},
watch: {
index(newIndex) {
if (!this.context) {
return;
}
this.context.index = newIndex;
},
item(newItem) {
if (!this.context) {
return;
}
this.context.layoutItem = newItem;
}
},
mounted() {
this.context = {
layoutItem: this.item,
index: this.index
};
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.initSelect);
},
destroyed() {
if (this.removeSelectable) {
this.removeSelectable();
}
}
};
</script>

View File

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

View File

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

View File

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

View File

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

View File

@ -20,13 +20,81 @@
* 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) {
@ -41,51 +109,7 @@ export default function DisplayLayoutPlugin(options) {
return domainObject.type === 'layout';
},
view: function (domainObject, objectPath) {
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();
}
};
return new DisplayLayoutView(openmct, domainObject, objectPath, options);
},
priority() {
return 100;

View File

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

View File

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

View File

@ -19,40 +19,41 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(function () {
/**
* An object containing key-value pairs, where keys are symbolic of
* device attributes, and values are functions that take the
* `agentService` as inputs and return boolean values indicating
* whether or not the current device has these attributes.
*
* For internal use by the mobile support bundle.
*
* @memberof platform/commonUI/mobile
* @private
*/
import HyperlinkLayout from './HyperlinkLayout.vue';
import Vue from 'vue';
export default function HyperlinkProvider(openmct) {
return {
mobile: function (agentService) {
return agentService.isMobile();
key: 'hyperlink.view',
name: 'Hyperlink',
cssClass: 'icon-chain-links',
canView(domainObject) {
return domainObject.type === 'hyperlink';
},
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();
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;
}
};
}
};
});
}

View File

@ -0,0 +1,89 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import HyperlinkProvider from './HyperlinkProvider';
export default function () {
return function install(openmct) {
openmct.types.addType('hyperlink', {
name: 'Hyperlink',
key: 'hyperlink',
description: 'A hyperlink to redirect to a different link',
creatable: true,
cssClass: 'icon-chain-links',
initialize: function (domainObject) {
domainObject.displayFormat = "link";
domainObject.linkTarget = "_self";
},
form: [
{
"key": "url",
"name": "URL",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayText",
"name": "Text to Display",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayFormat",
"name": "Display Format",
"control": "select",
"options": [
{
"name": "Link",
"value": "link"
},
{
"name": "Button",
"value": "button"
}
],
"cssClass": "l-inline"
},
{
"key": "linkTarget",
"name": "Tab to Open Hyperlink",
"control": "select",
"options": [
{
"name": "Open in this tab",
"value": "_self"
},
{
"name": "Open in a new tab",
"value": "_blank"
}
],
"cssClass": "l-inline"
}
]
});
openmct.objectViews.addProvider(new HyperlinkProvider(openmct));
};
}

View File

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

View File

@ -0,0 +1,37 @@
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;
}
}

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