Compare commits

..

2 Commits

Author SHA1 Message Date
Shefali Joshi
cfd7f2201a Merge branch 'master' into version-1.7.4 2021-06-07 11:26:37 -07:00
Joshi
97aebbf458 Prepare master for Sprint 1.7.4 2021-06-07 07:30:26 -07:00
219 changed files with 3967 additions and 7672 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
name: lighthouse
on:
workflow_dispatch:
inputs:
version:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.version }}
- 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

3
.gitignore vendored
View File

@@ -40,7 +40,4 @@ npm-debug.log
# karma reports
report.*.json
# Lighthouse reports
.lighthouseci
package-lock.json

14
API.md
View File

@@ -595,17 +595,9 @@ section.
#### Limit Evaluators **draft**
Limit evaluators allow a telemetry integrator to define which limits exist for a
telemetry endpoint and how limits should be applied to telemetry from a given domain object.
A limit evaluator can implement the `evalute` method which is used to define how limits
should be applied to telemetry and the `getLimits` method which is used to specify
what the limit values are for different limit levels.
Limit levels can be mapped to one of 5 colors for visualization:
`purple`, `red`, `orange`, `yellow` and `cyan`.
For an example of a limit evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.
Limit evaluators allow a telemetry integrator to define how limits should be
applied to telemetry from a given domain object. For an example of a limit
evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.
### Telemetry Consumer APIs **draft**

View File

@@ -1,11 +1,9 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript)
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0)
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
## See Open MCT in Action
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).

View File

@@ -26,26 +26,14 @@ define([
) {
var PURPLE = {
sin: 2.2,
cos: 2.2
},
RED = {
var RED = {
sin: 0.9,
cos: 0.9
},
ORANGE = {
sin: 0.7,
cos: 0.7
},
YELLOW = {
sin: 0.5,
cos: 0.5
},
CYAN = {
sin: 0.45,
cos: 0.45
},
LIMITS = {
rh: {
cssClass: "is-limit--upr is-limit--red",
@@ -106,66 +94,32 @@ define([
};
SinewaveLimitProvider.prototype.getLimits = function (domainObject) {
return {
limits: function () {
return Promise.resolve({
WATCH: {
low: {
color: "cyan",
sin: -CYAN.sin,
cos: -CYAN.cos
},
high: {
color: "cyan",
...CYAN
}
},
return {
WARNING: {
low: {
color: "yellow",
cssClass: "is-limit--lwr is-limit--yellow",
sin: -YELLOW.sin,
cos: -YELLOW.cos
},
high: {
color: "yellow",
cssClass: "is-limit--upr is-limit--yellow",
...YELLOW
}
},
DISTRESS: {
low: {
color: "orange",
sin: -ORANGE.sin,
cos: -ORANGE.cos
},
high: {
color: "orange",
...ORANGE
}
},
CRITICAL: {
low: {
color: "red",
cssClass: "is-limit--lwr is-limit--red",
sin: -RED.sin,
cos: -RED.cos
},
high: {
color: "red",
cssClass: "is-limit--upr is-limit--red",
...RED
}
},
SEVERE: {
low: {
color: "purple",
sin: -PURPLE.sin,
cos: -PURPLE.cos
},
high: {
color: "purple",
...PURPLE
}
}
});
};
}
};
};

View File

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

View File

@@ -49,10 +49,6 @@ 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('/');
@@ -63,9 +59,6 @@ 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

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

View File

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

View File

@@ -1,96 +0,0 @@
---
ci:
collect:
urls:
- http://localhost/
numberOfRuns: 5
settings:
onlyCategories:
- performance
- best-practices
upload:
target: temporary-public-storage
assert:
preset: lighthouse:recommended
assertions:
### Applicable assertions
bootup-time:
- warn
- minScore: 0.88 #Original value was calculated at 0.88
dom-size:
- error
- maxNumericValue: 200 #Original value was calculated at 188
first-contentful-paint:
- error
- minScore: 0.07 #Original value was calculated at 0.08
mainthread-work-breakdown:
- warn
- minScore: 0.8 #Original value was calculated at 0.8
unused-javascript:
- warn
- maxLength: 1
- error
- maxNumericValue: 2000 #Original value was calculated at 1855
unused-css-rules: warn
installable-manifest: warn
service-worker: warn
### Disabled seo, accessibility, and pwa assertions, below
categories:seo: 'off'
categories:accessibility: 'off'
categories:pwa: 'off'
accesskeys: 'off'
apple-touch-icon: 'off'
aria-allowed-attr: 'off'
aria-command-name: 'off'
aria-hidden-body: 'off'
aria-hidden-focus: 'off'
aria-input-field-name: 'off'
aria-meter-name: 'off'
aria-progressbar-name: 'off'
aria-required-attr: 'off'
aria-required-children: 'off'
aria-required-parent: 'off'
aria-roles: 'off'
aria-toggle-field-name: 'off'
aria-tooltip-name: 'off'
aria-treeitem-name: 'off'
aria-valid-attr: 'off'
aria-valid-attr-value: 'off'
button-name: 'off'
bypass: 'off'
canonical: 'off'
color-contrast: 'off'
content-width: 'off'
crawlable-anchors: 'off'
csp-xss: 'off'
font-display: 'off'
font-size: 'off'
maskable-icon: 'off'
heading-order: 'off'
hreflang: 'off'
html-has-lang: 'off'
html-lang-valid: 'off'
http-status-code: 'off'
image-alt: 'off'
input-image-alt: 'off'
is-crawlable: 'off'
label: 'off'
link-name: 'off'
link-text: 'off'
list: 'off'
listitem: 'off'
meta-description: 'off'
meta-refresh: 'off'
meta-viewport: 'off'
object-alt: 'off'
plugins: 'off'
robots-txt: 'off'
splash-screen: 'off'
tabindex: 'off'
tap-targets: 'off'
td-headers-attr: 'off'
th-has-data-cells: 'off'
themed-omnibox: 'off'
valid-lang: 'off'
video-caption: 'off'
viewport: 'off'

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "1.7.6-SNAPSHOT",
"version": "1.7.4-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {
@@ -34,21 +34,20 @@
"git-rev-sync": "^1.4.0",
"glob": ">= 3.0.0",
"html-loader": "^0.5.5",
"html2canvas": "^1.0.0-rc.7",
"html2canvas": "^1.0.0-alpha.12",
"imports-loader": "^0.8.0",
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.7.1",
"jasmine-core": "^3.1.0",
"jsdoc": "^3.3.2",
"karma": "6.3.4",
"karma": "5.1.1",
"karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "2.1.1",
"karma-firefox-launcher": "1.3.0",
"karma-cli": "2.0.0",
"karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-junit-reporter": "2.0.1",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "4.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-jasmine": "3.3.1",
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "4.0.2",
"location-bar": "^3.0.1",
"lodash": "^4.17.12",
@@ -61,7 +60,7 @@
"moment-timezone": "0.5.28",
"node-bourbon": "^4.2.3",
"node-sass": "^4.14.1",
"painterro": "^1.2.56",
"painterro": "^1.0.35",
"printj": "^1.2.1",
"raw-loader": "^0.5.1",
"request": "^2.69.0",
@@ -90,7 +89,6 @@
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
"test:coverage:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"verify": "concurrently 'npm:test' 'npm:lint'",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
@@ -102,9 +100,6 @@
"type": "git",
"url": "https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=10.10.2 <16.0.0"
},
"author": "",
"license": "Apache-2.0",
"private": true

View File

@@ -24,6 +24,7 @@ define([
"./src/navigation/NavigationService",
"./src/navigation/NavigateAction",
"./src/navigation/OrphanNavigationHandler",
"./src/windowing/NewTabAction",
"./res/templates/browse.html",
"./res/templates/browse-object.html",
"./res/templates/browse/object-header.html",
@@ -36,6 +37,7 @@ define([
NavigationService,
NavigateAction,
OrphanNavigationHandler,
NewTabAction,
browseTemplate,
browseObjectTemplate,
objectHeaderTemplate,
@@ -126,6 +128,23 @@ define([
"depends": [
"navigationService"
]
},
{
"key": "window",
"name": "Open In New Tab",
"implementation": NewTabAction,
"description": "Open in a new browser tab",
"category": [
"view-control",
"contextual"
],
"depends": [
"urlService",
"$window"
],
"group": "windowing",
"priority": 10,
"cssClass": "icon-new-window"
}
],
"runs": [

View File

@@ -20,40 +20,39 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import HyperlinkLayout from './HyperlinkLayout.vue';
import Vue from 'vue';
/**
* Module defining NewTabAction (Originally NewWindowAction). Created by vwoeltje on 11/18/14.
*/
define(
[],
function () {
/**
* The new tab action allows a domain object to be opened
* into a new browser tab.
* @memberof platform/commonUI/browse
* @constructor
* @implements {Action}
*/
function NewTabAction(urlService, $window, context) {
context = context || {};
export default function HyperlinkProvider(openmct) {
return {
key: 'hyperlink.view',
name: 'Hyperlink',
cssClass: 'icon-chain-links',
canView(domainObject) {
return domainObject.type === 'hyperlink';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
HyperlinkLayout
},
provide: {
domainObject
},
template: '<hyperlink-layout></hyperlink-layout>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
this.urlService = urlService;
this.open = function () {
arguments[0] += "&hideTree=true&hideInspector=true";
$window.open.apply($window, arguments);
};
// Choose the object to be opened into a new tab
this.domainObject = context.selectedObject || context.domainObject;
}
};
}
NewTabAction.prototype.perform = function () {
this.open(
this.urlService.urlForNewTab("browse", this.domainObject),
"_blank"
);
};
return NewTabAction;
}
);

View File

@@ -0,0 +1,75 @@
/*****************************************************************************
* 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/windowing/NewTabAction"],
function (NewTabAction) {
describe("The new tab action", function () {
var actionSelected,
actionCurrent,
mockWindow,
mockContextCurrent,
mockContextSelected,
mockUrlService;
beforeEach(function () {
mockWindow = jasmine.createSpyObj("$window", ["open", "location"]);
// Context if the current object is selected
// For example, when the top right new tab
// button is clicked, the user is using the
// current domainObject
mockContextCurrent = jasmine.createSpyObj("context", ["domainObject"]);
// Context if the selected object is selected
// For example, when an object in the left
// tree is opened in a new tab using the
// context menu
mockContextSelected = jasmine.createSpyObj("context", ["selectedObject",
"domainObject"]);
// Mocks the urlService used to make the new tab's url from a
// domainObject and mode
mockUrlService = jasmine.createSpyObj("urlService", ["urlForNewTab"]);
// Action done using the current context or mockContextCurrent
actionCurrent = new NewTabAction(mockUrlService, mockWindow,
mockContextCurrent);
// Action done using the selected context or mockContextSelected
actionSelected = new NewTabAction(mockUrlService, mockWindow,
mockContextSelected);
});
it("new tab with current url is opened", function () {
actionCurrent.perform();
});
it("new tab with a selected url is opened", function () {
actionSelected.perform();
});
});
}
);

View File

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

View File

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

View File

@@ -0,0 +1,120 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./src/HyperlinkController',
'./res/templates/hyperlink.html'
], function (
HyperlinkController,
hyperlinkTemplate
) {
return {
name: "platform/features/hyperlink",
definition: {
"name": "Hyperlink",
"description": "Insert a hyperlink to reference a link",
"extensions": {
"types": [
{
"key": "hyperlink",
"name": "Hyperlink",
"cssClass": "icon-chain-links",
"description": "A hyperlink to redirect to a different link",
"features": ["creation"],
"properties": [
{
"key": "url",
"name": "URL",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayText",
"name": "Text to Display",
"control": "textfield",
"required": true,
"cssClass": "l-input-lg"
},
{
"key": "displayFormat",
"name": "Display Format",
"control": "select",
"options": [
{
"name": "Link",
"value": "link"
},
{
"value": "button",
"name": "Button"
}
],
"cssClass": "l-inline"
},
{
"key": "openNewTab",
"name": "Tab to Open Hyperlink",
"control": "select",
"options": [
{
"name": "Open in this tab",
"value": "thisTab"
},
{
"value": "newTab",
"name": "Open in a new tab"
}
],
"cssClass": "l-inline"
}
],
"model": {
"displayFormat": "link",
"openNewTab": "thisTab",
"removeTitle": true
}
}
],
"views": [
{
"key": "hyperlink",
"type": "hyperlink",
"name": "Hyperlink Display",
"template": hyperlinkTemplate,
"editable": false
}
],
"controllers": [
{
"key": "HyperlinkController",
"implementation": HyperlinkController,
"depends": ["$scope"]
}
]
}
}
};
});

View File

@@ -1,5 +1,5 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
@@ -19,34 +19,10 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<li class="c-inspect-properties__row">
<div class="c-inspect-properties__label">
{{ label }}
</div>
<div class="c-inspect-properties__value">
{{ value }}
</div>
</li>
</template>
<script>
export default {
props: {
label: {
type: String,
default() {
return '';
}
},
value: {
type: String,
default() {
return '';
}
}
}
};
</script>
<a class="c-hyperlink u-links" ng-controller="HyperlinkController as hyperlink" href="{{domainObject.getModel().url}}"
ng-attr-target="{{hyperlink.openNewTab() ? '_blank' : undefined}}"
ng-class="{
'c-hyperlink--button u-fills-container' : hyperlink.isButton(),
'c-hyperlink--link' : !hyperlink.isButton() }">
<span class="c-hyperlink__label">{{domainObject.getModel().displayText}}</span>
</a>

View File

@@ -0,0 +1,61 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* This bundle adds the Hyperlink object type, which can be used to add hyperlinks as a domain Object type
and into display Layouts as either a button or link that can be chosen to open in either the same tab or
create a new tab to open the link in
* @namespace platform/features/hyperlink
*/
define(
[],
function () {
function HyperlinkController($scope) {
this.$scope = $scope;
}
/**Function to analyze the location in which to open the hyperlink
@returns true if the hyperlink is chosen to open in a different tab, false if the same tab
**/
HyperlinkController.prototype.openNewTab = function () {
if (this.$scope.domainObject.getModel().openNewTab === "thisTab") {
return false;
} else {
return true;
}
};
/**Function to specify the format in which the hyperlink should be created
@returns true if the hyperlink is chosen to be created as a button, false if a link
**/
HyperlinkController.prototype.isButton = function () {
if (this.$scope.domainObject.getModel().displayFormat === "link") {
return false;
}
return true;
};
return HyperlinkController;
}
);

View File

@@ -0,0 +1,89 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../src/HyperlinkController"],
function (HyperlinkController) {
describe("The controller for hyperlinks", function () {
var domainObject,
controller,
scope;
beforeEach(function () {
scope = jasmine.createSpyObj(
"$scope",
["domainObject"]
);
domainObject = jasmine.createSpyObj(
"domainObject",
["getModel"]
);
scope.domainObject = domainObject;
controller = new HyperlinkController(scope);
});
it("knows when it should open a new tab", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "link",
"openNewTab": "newTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.openNewTab())
.toBe(true);
});
it("knows when it is a button", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "button",
"openNewTab": "thisTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.isButton())
.toEqual(true);
});
it("knows when it should open in the same tab", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "link",
"openNewTab": "thisTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.openNewTab())
.toBe(false);
});
it("knows when it is a link", function () {
scope.domainObject.getModel.and.returnValue({
"displayFormat": "link",
"openNewTab": "thisTab",
"showTitle": false
}
);
controller = new HyperlinkController(scope);
expect(controller.openNewTab())
.toBe(false);
});
});
}
);

View File

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

View File

@@ -20,10 +20,33 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ViewLargeAction from './viewLargeAction.js';
export default function plugin() {
return function install(openmct) {
openmct.actions.register(new ViewLargeAction(openmct));
define([
"./res/templates/deprecated-timeline-message.html"
], function (
deprecatedTimelineMessage
) {
return {
name: 'platform/features/timeline',
definition: {
extensions: {
types: [
{
key: "timeline",
name: "Timeline",
description: "Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported. (07/18/2018)",
priority: 502
}
],
views: [
{
key: "timeline",
name: "Timeline",
type: "timeline",
description: "Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported. (07/18/2018)",
template: deprecatedTimelineMessage
}
]
}
}
};
}
});

View File

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

View File

@@ -122,7 +122,6 @@ define([
}
};
this.destroy = this.destroy.bind(this);
/**
* Tracks current selection state of the application.
* @private
@@ -263,7 +262,7 @@ define([
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable.default());
this.install(this.plugins.TelemetryTable());
this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin());
this.install(LicensesPlugin.default());
@@ -275,7 +274,6 @@ define([
this.install(ImageryPlugin.default());
this.install(this.plugins.FlexibleLayout());
this.install(this.plugins.GoToOriginalAction());
this.install(this.plugins.OpenInNewTabAction());
this.install(this.plugins.ImportExport());
this.install(this.plugins.WebPage());
this.install(this.plugins.Condition());
@@ -284,7 +282,6 @@ define([
this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
this.install(this.plugins.ViewLargeAction());
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.NonEditableFolder());
}
@@ -436,8 +433,6 @@ define([
Browse(this);
}
window.addEventListener('beforeunload', this.destroy);
this.router.start();
this.emit('start');
}.bind(this));
@@ -461,7 +456,6 @@ define([
};
MCT.prototype.destroy = function () {
window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy');
this.router.destroy();
};

View File

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

View File

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

View File

@@ -0,0 +1,186 @@
/*****************************************************************************
* 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 {string} type of image to convert the element to.
* @returns {promise}
*/
ExportImageService.prototype.renderElement = function (element, imageType, className) {
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) {
return canvas.toBlob(resolve, 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();
}
}]
});
});
};
/**
* 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, "jpg", className).then(function (img) {
saveAs(img, 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, "png", className).then(function (img) {
saveAs(img, 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, className) {
return this.renderElement(element, "png", className);
};
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
return filename.replace(regex, '_');
}
/**
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
* implements the method in browsers that would not otherwise support it.
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
*/
function polyfillToBlob() {
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", {
value: function (callback, mimeType, quality) {
const binStr = atob(this.toDataURL(mimeType, quality).split(',')[1]);
const len = binStr.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], {type: mimeType || "image/png"}));
}
});
}
}
polyfillToBlob();
return ExportImageService;
}
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,8 +45,6 @@ function ObjectAPI(typeRegistry, openmct) {
this.rootProvider = new RootObjectProvider(this.rootRegistry);
this.cache = {};
this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
}
/**
@@ -399,25 +397,20 @@ ObjectAPI.prototype._toMutable = function (object) {
mutableObject = object;
} else {
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
}
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier);
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier);
if (provider !== undefined
&& provider.observe !== undefined
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
let unobserve = provider.observe(identifier, (updatedModel) => {
if (updatedModel.persisted > mutableObject.modified) {
//Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
//in rapid succession and intermediate persistence states are returned by the observe function.
mutableObject.$refresh(updatedModel);
}
});
mutableObject.$on('$_destroy', () => {
unobserve();
});
}
if (provider !== undefined
&& provider.observe !== undefined) {
let unobserve = provider.observe(identifier, (updatedModel) => {
mutableObject.$refresh(updatedModel);
});
mutableObject.$on('$destroy', () => {
unobserve();
});
}
return mutableObject;

View File

@@ -163,22 +163,14 @@ describe("The Object API", () => {
key: 'test-key'
},
name: 'test object',
type: 'notebook',
otherAttribute: 'other-attribute-value',
modified: 0,
persisted: 0,
objectAttribute: {
embeddedObject: {
embeddedKey: 'embedded-value'
}
}
};
updatedTestObject = Object.assign({
otherAttribute: 'changed-attribute-value'
}, testObject);
updatedTestObject.modified = 1;
updatedTestObject.persisted = 1;
updatedTestObject = Object.assign({otherAttribute: 'changed-attribute-value'}, testObject);
mockProvider = jasmine.createSpyObj("mock provider", [
"get",
"create",
@@ -190,8 +182,6 @@ describe("The Object API", () => {
mockProvider.observeObjectChanges.and.callFake(() => {
callbacks[0](updatedTestObject);
callbacks.splice(0, 1);
return () => {};
});
mockProvider.observe.and.callFake((id, callback) => {
if (callbacks.length === 0) {
@@ -199,8 +189,6 @@ describe("The Object API", () => {
} else {
callbacks[0] = callback;
}
return () => {};
});
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);

View File

@@ -21,7 +21,8 @@
&__outer {
@include abs();
background: $colorBodyBg;
background: $overlayColorBg;
color: $overlayColorFg;
display: flex;
flex-direction: column;
padding: $overlayInnerMargin;
@@ -29,6 +30,7 @@
&__close-button {
$p: $interiorMargin + 2px;
color: $overlayColorFg;
font-size: 1.5em;
position: absolute;
top: $p; right: $p;
@@ -80,6 +82,11 @@
}
}
.c-button,
.c-click-icon {
filter: $overlayBrightnessAdjust;
}
.c-object-label__name {
filter: $objectLabelNameFilter;
}
@@ -96,7 +103,6 @@ body.desktop {
}
// Overlay types, styling for desktop. Appended to .l-overlay-wrapper element.
.l-overlay-large,
.l-overlay-small,
.l-overlay-fit {
.c-overlay__outer {
@@ -118,8 +124,12 @@ body.desktop {
$tbPad: floor($pad * 0.8);
$lrPad: $pad;
.c-overlay {
&__blocker {
display: none;
}
&__outer {
@include overlaySizing($overlayOuterMarginLarge);
@include overlaySizing($overlayOuterMarginFullscreen);
padding: $tbPad $lrPad;
}

View File

@@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { TelemetryCollection } = require("./TelemetryCollection");
define([
'../../plugins/displayLayout/CustomStringFormatter',
'./TelemetryMetadataManager',
@@ -275,28 +273,6 @@ define([
}
};
/**
* Request telemetry collection for a domain object.
* The `options` argument allows you to specify filters
* (start, end, etc.), sort order, and strategies for retrieving
* telemetry (aggregation, latest available, etc.).
*
* @method requestTelemetryCollection
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
* @param {module:openmct.DomainObject} domainObject the object
* which has associated telemetry
* @param {module:openmct.TelemetryAPI~TelemetryRequest} options
* options for this telemetry collection request
* @returns {TelemetryCollection} a TelemetryCollection instance
*/
TelemetryAPI.prototype.requestTelemetryCollection = function (domainObject, options = {}) {
return new TelemetryCollection(
this.openmct,
domainObject,
options
);
};
/**
* Request historical telemetry for a domain object.
* The `options` argument allows you to specify filters
@@ -591,24 +567,21 @@ define([
* @method limits returns a limits object of
* type {
* level1: {
* low: { key1: value1, key2: value2, color: <supportedColor> },
* high: { key1: value1, key2: value2, color: <supportedColor> }
* low: { key1: value1, key2: value2 },
* high: { key1: value1, key2: value2 }
* },
* level2: {
* low: { key1: value1, key2: value2 },
* high: { key1: value1, key2: value2 }
* }
* }
* supported colors are purple, red, orange, yellow and cyan
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
TelemetryAPI.prototype.getLimits = function (domainObject) {
const provider = this.findLimitEvaluator(domainObject);
if (!provider || !provider.getLimits) {
return {
limits: function () {
return Promise.resolve(undefined);
}
limits: function () {}
};
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -73,7 +73,7 @@ describe('the plugin', function () {
});
it('provides a folder to hold plans', () => {
return openmct.objects.get(identifier).then((object) => {
openmct.objects.get(identifier).then((object) => {
expect(object).toEqual({
identifier,
type: 'folder',
@@ -83,7 +83,7 @@ describe('the plugin', function () {
});
it('provides composition for couch search folders', () => {
return composition.load().then((objects) => {
composition.load().then((objects) => {
expect(objects.length).toEqual(2);
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -67,6 +67,10 @@ describe("The LAD Table", () => {
// this setups up the app
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
parent = document.createElement('div');
@@ -86,7 +90,7 @@ describe("The LAD Table", () => {
});
openmct.on('start', done);
openmct.startHeadless();
openmct.startHeadless(appHolder);
});
afterEach(() => {
@@ -109,8 +113,7 @@ describe("The LAD Table", () => {
beforeEach(() => {
ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable);
return ladTableCompositionCollection.load();
ladTableCompositionCollection.load();
});
it("should accept telemetry producing objects", () => {
@@ -189,6 +192,8 @@ describe("The LAD Table", () => {
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick();
return;
});
it("should show one row per object in the composition", () => {
@@ -237,6 +242,13 @@ describe("The LAD Table Set", () => {
let ladPlugin;
let parent;
let child;
let telemetryCount = 3;
let timeFormat = 'utc';
let mockTelemetry = getMockTelemetry({
count: telemetryCount,
format: timeFormat
});
let mockObj = getMockObjects({
objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry']
@@ -252,22 +264,31 @@ describe("The LAD Table Set", () => {
mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier);
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
parent = document.createElement('div');
child = document.createElement('div');
parent.appendChild(child);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
ladPlugin = new LadPlugin();
openmct.install(ladPlugin);
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
openmct.time.bounds({
start: bounds.start,
end: bounds.end
});
openmct.on('start', done);
openmct.startHeadless();
openmct.start(appHolder);
});
afterEach(() => {
@@ -280,8 +301,6 @@ describe("The LAD Table Set", () => {
});
it("should provide a lad table set view only for lad table set objects", () => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
let ladTableSetView = applicableViews.find(
@@ -296,11 +315,8 @@ describe("The LAD Table Set", () => {
let ladTableSetCompositionCollection;
beforeEach(() => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);
return ladTableSetCompositionCollection.load();
ladTableSetCompositionCollection.load();
});
it("should accept lad table objects", () => {
@@ -338,17 +354,41 @@ describe("The LAD Table Set", () => {
otherObj.ladTable.composition.push(mockObj.telemetry.identifier);
mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier);
beforeEach(() => {
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
beforeEach(async () => {
let telemetryRequestResolve;
let ladObjectResolve;
let anotherLadObjectResolve;
spyOn(openmct.objects, 'get').and.callFake((obj) => {
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
});
let ladObjectPromise = new Promise((resolve) => {
ladObjectResolve = resolve;
});
let anotherLadObjectPromise = new Promise((resolve) => {
anotherLadObjectResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise;
});
openmct.objects.get.and.callFake((obj) => {
if (obj.key === 'lad-object') {
return Promise.resolve(mockObj.ladTable);
ladObjectResolve(mockObj.ladObject);
return ladObjectPromise;
} else if (obj.key === 'another-lad-object') {
return Promise.resolve(otherObj.ladTable);
} else if (obj.key === 'telemetry-object') {
return Promise.resolve(mockObj.telemetry);
anotherLadObjectResolve(otherObj.ladObject);
return anotherLadObjectPromise;
}
return Promise.resolve({});
});
openmct.time.bounds({
@@ -359,19 +399,20 @@ describe("The LAD Table Set", () => {
applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey);
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
ladTableSetView.show(child);
ladTableSetView.show(child, true);
return Vue.nextTick();
await Promise.all([telemetryRequestPromise, ladObjectPromise, anotherLadObjectPromise]);
await Vue.nextTick();
return;
});
it("should show one row per lad table object in the composition", () => {
const ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);
const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length;
return ladTableSetCompositionCollection.load().then(() => {
const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length;
expect(rowCount).toBe(mockObj.ladTableSet.composition.length);
});
expect(rowCount).toBe(mockObj.ladTableSet.composition.length);
pending();
});
});
});

View File

@@ -22,14 +22,12 @@
define(
[
"utils/testing",
"./URLIndicator",
"./URLIndicatorPlugin",
"../../MCT",
"zepto"
],
function (
testingUtils,
URLIndicator,
URLIndicatorPlugin,
MCT,
@@ -46,7 +44,7 @@ define(
beforeEach(function () {
jasmine.clock().install();
openmct = new testingUtils.createOpenMct();
openmct = new MCT();
spyOn(openmct.indicators, 'add');
spyOn($, 'ajax');
$.ajax.and.callFake(function (options) {
@@ -57,8 +55,6 @@ define(
afterEach(function () {
$.ajax = defaultAjaxFunction;
jasmine.clock().uninstall();
return testingUtils.resetApplicationState(openmct);
});
describe("on initialization", function () {

View File

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

View File

@@ -28,10 +28,8 @@ import {
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
import Vue from 'vue';
// TODO lots of its without expects
xdescribe("AutoflowTabularPlugin", () => {
describe("AutoflowTabularPlugin", () => {
let testType;
let testObject;
let mockmct;
@@ -53,7 +51,7 @@ xdescribe("AutoflowTabularPlugin", () => {
});
afterEach(() => {
return resetApplicationState(mockmct);
resetApplicationState(mockmct);
});
it("installs a view provider", () => {
@@ -103,7 +101,7 @@ xdescribe("AutoflowTabularPlugin", () => {
});
}
beforeEach(() => {
beforeEach((done) => {
callbacks = {};
spyOnBuiltins(['requestAnimationFrame']);
@@ -182,7 +180,7 @@ xdescribe("AutoflowTabularPlugin", () => {
view = provider.view(testObject);
view.show(testContainer);
return Vue.nextTick();
return done();
});
afterEach(() => {

View File

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

View File

@@ -27,17 +27,15 @@ export default class StyleRuleManager extends EventEmitter {
super();
this.openmct = openmct;
this.callback = callback;
this.refreshData = this.refreshData.bind(this);
this.toggleSubscription = this.toggleSubscription.bind(this);
if (suppressSubscriptionOnEdit) {
this.openmct.editor.on('isEditing', this.toggleSubscription);
this.openmct.editor.on('isEditing', this.toggleSubscription.bind(this));
this.isEditing = this.openmct.editor.editing;
}
if (styleConfiguration) {
this.initialize(styleConfiguration);
if (styleConfiguration.conditionSetIdentifier) {
this.openmct.time.on("bounds", this.refreshData);
this.openmct.time.on("bounds", this.refreshData.bind(this));
this.subscribeToConditionSet();
} else {
this.applyStaticStyle();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,10 +21,6 @@
margin-left: $interiorMargin;
}
&__value {
@include isLimit();
}
.c-frame & {
@include abs();
border: 1px solid transparent;

View File

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

View File

@@ -37,15 +37,7 @@ export default class DuplicateAction {
let duplicationTask = new DuplicateTask(this.openmct);
let originalObject = objectPath[0];
let parent = objectPath[1];
let userInput;
try {
userInput = await this.getUserInput(originalObject, parent);
} catch (error) {
// user most likely canceled
return;
}
let userInput = await this.getUserInput(originalObject, parent);
let newParent = userInput.location;
let inNavigationPath = this.inNavigationPath(originalObject);
@@ -79,8 +71,7 @@ export default class DuplicateAction {
updateNameCheck(object, name) {
if (object.name !== name) {
object.name = name;
this.openmct.objects.save(object);
this.openmct.objects.mutate(object, 'name', name);
}
}
@@ -104,7 +95,7 @@ export default class DuplicateAction {
cssClass: "l-input-lg"
},
{
name: "Location",
name: "location",
cssClass: "grows",
control: "locator",
validate: this.validate(object, parent),

View File

@@ -121,9 +121,10 @@ describe("The Duplicate Action plugin", () => {
describe("when moving an object to a new parent", () => {
beforeEach(async () => {
beforeEach(async (done) => {
duplicateTask = new DuplicateTask(openmct);
await duplicateTask.duplicate(parentObject, anotherParentObject);
done();
});
it("the duplicate child object's name (when not changing) should be the same as the original object", async () => {
@@ -142,15 +143,15 @@ describe("The Duplicate Action plugin", () => {
});
describe("when a new name is provided for the duplicated object", () => {
it("the name is updated", () => {
const NEW_NAME = 'New Name';
let childName;
const NEW_NAME = 'New Name';
beforeEach(() => {
duplicateTask = new DuplicateAction(openmct);
duplicateTask.updateNameCheck(parentObject, NEW_NAME);
});
childName = parentObject.name;
it("the name is updated", () => {
let childName = parentObject.name;
expect(childName).toEqual(NEW_NAME);
});
});

View File

@@ -23,11 +23,6 @@
body.mobile & {
flex: 1 0 auto;
}
[class*='l-overlay'] & {
// When this view is in an overlay, prevent navigation
pointer-events: none;
}
}
/******************************* GRID ITEMS */

View File

@@ -22,9 +22,4 @@
@include isAlias();
}
}
[class*='l-overlay'] & {
// When this view is in an overlay, prevent navigation
pointer-events: none;
}
}

View File

@@ -24,15 +24,10 @@ import {
resetApplicationState
} from 'utils/testing';
describe("the goToOriginalAction plugin", () => {
describe("the plugin", () => {
let openmct;
let goToOriginalAction;
let mockRootFolder;
let mockSubFolder;
let mockSubSubFolder;
let mockObject;
let goToFolderAction;
let mockObjectPath;
let hash;
beforeEach((done) => {
openmct = createOpenMct();
@@ -40,7 +35,7 @@ describe("the goToOriginalAction plugin", () => {
openmct.on('start', done);
openmct.startHeadless();
goToOriginalAction = openmct.actions._allActions.goToOriginal;
goToFolderAction = openmct.actions._allActions.goToOriginal;
});
afterEach(() => {
@@ -48,153 +43,34 @@ describe("the goToOriginalAction plugin", () => {
});
it('installs the go to folder action', () => {
expect(goToOriginalAction).toBeDefined();
expect(goToFolderAction).toBeDefined();
});
describe('when invoked', () => {
beforeEach(() => {
mockRootFolder = getMockObject('mock-root');
mockSubFolder = getMockObject('mock-sub');
mockSubSubFolder = getMockObject('mock-sub-sub');
mockObject = getMockObject('mock-table');
mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({
identifier: {
namespace: '',
key: 'test'
}
}));
mockObjectPath = [
mockObject,
mockSubSubFolder,
mockSubFolder,
mockRootFolder
];
spyOn(openmct.objects, 'get').and.callFake(identifier => {
const mockedObject = getMockObject(identifier);
return Promise.resolve(mockedObject);
});
spyOn(openmct.router, 'navigate').and.callFake(navigateTo => {
hash = navigateTo;
});
return goToOriginalAction.invoke(mockObjectPath);
goToFolderAction.invoke(mockObjectPath);
});
it('goes to the original location', () => {
const originalLocationHash = '#/browse/mock-root/mock-table';
return waitForNavigation(() => {
return hash === originalLocationHash;
}).then(() => {
expect(hash).toEqual(originalLocationHash);
});
it('goes to the original location', (done) => {
setTimeout(() => {
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc');
done();
}, 1500);
});
});
function waitForNavigation(navigated) {
return new Promise((resolve, reject) => {
const start = Date.now();
checkNavigated();
function checkNavigated() {
const elapsed = Date.now() - start;
if (navigated()) {
resolve();
} else if (elapsed >= jasmine.DEFAULT_TIMEOUT_INTERVAL - 1000) {
reject("didn't navigate in time");
} else {
setTimeout(checkNavigated);
}
}
});
}
function getMockObject(key) {
const id = typeof key === 'string' ? key : key.key;
const mockMCTObjects = {
"ROOT": {
"composition": [
{
"namespace": "",
"key": "mock-root"
}
],
"identifier": {
"namespace": "",
"key": "mock-root"
}
},
"mock-root": {
"composition": [
{
"namespace": "",
"key": "mock-sub"
},
{
"namespace": "",
"key": "mock-table"
}
],
"name": "root",
"type": "folder",
"id": "mock-root",
"location": "ROOT",
"identifier": {
"namespace": "",
"key": "mock-root"
}
},
"mock-sub": {
"composition": [
{
"namespace": "",
"key": "mock-sub-sub"
},
{
"namespace": "",
"key": "mock-table"
}
],
"name": "sub",
"type": "folder",
"location": "mock-root",
"identifier": {
"namespace": "",
"key": "mock-sub"
}
},
"mock-table": {
"composition": [],
"configuration": {
"columnWidths": {},
"hiddenColumns": {}
},
"name": "table",
"type": "table",
"location": "mock-root",
"identifier": {
"namespace": "",
"key": "mock-table"
}
},
"mock-sub-sub": {
"composition": [
{
"namespace": "",
"key": "mock-table"
}
],
"name": "sub sub",
"type": "folder",
"location": "mock-sub",
"identifier": {
"namespace": "",
"key": "mock-sub-sub"
}
}
};
return mockMCTObjects[id];
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,7 @@
<template>
<div
class="c-compass"
:style="`width: 100%; height: 100%`"
:style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`"
>
<CompassHUD
v-if="hasCameraFieldOfView"
@@ -33,12 +33,13 @@
/>
<CompassRose
v-if="hasCameraFieldOfView"
:heading="heading"
:sized-image-width="sizedImageDimensions.width"
:sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan"
:compass-rose-sizing-classes="compassRoseSizingClasses"
:heading="heading"
:sized-image-dimensions="sizedImageDimensions"
:sun-heading="sunHeading"
:lock-compass="lockCompass"
@toggle-lock-compass="toggleLockCompass"
/>
</div>
</template>
@@ -55,20 +56,42 @@ export default {
CompassRose
},
props: {
compassRoseSizingClasses: {
type: String,
containerWidth: {
type: Number,
required: true
},
containerHeight: {
type: Number,
required: true
},
naturalAspectRatio: {
type: Number,
required: true
},
image: {
type: Object,
required: true
},
sizedImageDimensions: {
type: Object,
lockCompass: {
type: Boolean,
required: true
}
},
computed: {
sizedImageDimensions() {
let sizedImageDimensions = {};
if ((this.containerWidth / this.containerHeight) > this.naturalAspectRatio) {
// container is wider than image
sizedImageDimensions.width = this.containerHeight * this.naturalAspectRatio;
sizedImageDimensions.height = this.containerHeight;
} else {
// container is taller than image
sizedImageDimensions.width = this.containerWidth;
sizedImageDimensions.height = this.containerWidth * this.naturalAspectRatio;
}
return sizedImageDimensions;
},
hasCameraFieldOfView() {
return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
},

View File

@@ -21,203 +21,152 @@
*****************************************************************************/
<template>
<div ref="compassRoseWrapper"
class="w-direction-rose"
:class="compassRoseSizingClasses"
@click="toggleLockCompass"
<div
class="w-direction-rose"
:class="compassRoseSizingClasses"
>
<svg ref="compassRoseSvg"
class="c-compass-rose-svg"
viewBox="0 0 100 100"
<div
class="c-direction-rose"
@click="toggleLockCompass"
>
<mask id="mask0"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="100"
height="100"
<div
class="c-nsew"
:style="compassRoseStyle"
>
<circle cx="50"
cy="50"
r="50"
fill="black"
/>
</mask>
<g class="c-cr__compass-wrapper">
<g class="c-cr__compass-main"
mask="url(#mask0)"
<svg
class="c-nsew__minor-ticks"
viewBox="0 0 100 100"
>
<!-- Background and clipped elements -->
<rect class="c-cr__bg"
width="100"
height="100"
fill="black"
<rect
class="c-nsew__tick c-tick-ne"
x="49"
y="0"
width="2"
height="5"
/>
<rect class="c-cr__edge"
width="100"
height="100"
fill="url(#paint0_radial)"
<rect
class="c-nsew__tick c-tick-se"
x="95"
y="49"
width="5"
height="2"
/>
<rect v-if="hasSunHeading"
class="c-cr__sun"
width="100"
height="100"
fill="url(#paint1_radial)"
:style="sunHeadingStyle"
<rect
class="c-nsew__tick c-tick-sw"
x="49"
y="95"
width="2"
height="5"
/>
<rect
class="c-nsew__tick c-tick-nw"
x="0"
y="49"
width="5"
height="2"
/>
<!-- Camera FOV -->
<mask id="mask2"
class="c-cr__cam-fov-l-mask"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="50"
height="100"
>
<rect width="51"
height="100"
/>
</mask>
<mask id="mask1"
class="c-cr__cam-fov-r-mask"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="50"
y="0"
width="50"
height="100"
>
<rect x="49"
width="51"
height="100"
/>
</mask>
<g class="c-cr__cam-fov"
:style="cameraPanStyle"
>
<g mask="url(#mask2)">
<rect class="c-cr__cam-fov-r"
x="49"
width="51"
height="100"
:style="cameraFOVStyleRightHalf"
/>
</g>
<g mask="url(#mask1)">
<rect class="c-cr__cam-fov-l"
width="51"
height="100"
:style="cameraFOVStyleLeftHalf"
/>
</g>
</g>
</g>
</svg>
<!-- Spacecraft body -->
<path v-if="hasHeading"
class="c-cr__spacecraft-body"
fill-rule="evenodd"
clip-rule="evenodd"
d="M37 49C35.3431 49 34 50.3431 34 52V82C34 83.6569 35.3431 85 37 85H63C64.6569 85 66 83.6569 66 82V52C66 50.3431 64.6569 49 63 49H37ZM50 52L58 60H55V67H45V60H42L50 52Z"
:style="headingStyle"
/>
<svg
class="c-nsew__ticks"
viewBox="0 0 100 100"
>
<polygon
class="c-nsew__tick c-tick-n"
points="50,0 60,10 40,10"
/>
<rect
class="c-nsew__tick c-tick-e"
x="95"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-w"
x="0"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-s"
x="49"
y="95"
width="2"
height="5"
/>
<!-- NSEW and ticks -->
<g class="c-cr__nsew"
:style="compassRoseStyle"
>
<g class="c-cr__ticks-major">
<path d="M50 3L43 10H57L50 3Z" />
<path d="M4 51V49H10V51H4Z"
class="--hide-min"
/>
<path d="M49 96V90H51V96H49Z"
class="--hide-min"
/>
<path d="M90 49V51H96V49H90Z"
class="--hide-min"
/>
</g>
<g class="c-cr__ticks-minor --hide-small">
<path d="M4 51V49H10V51H4Z" />
<path d="M90 51V49H96V51H90Z" />
<path d="M51 96H49V90H51V96Z" />
<path d="M51 10L49 10V4L51 4V10Z" />
</g>
<g class="c-cr__nsew-text">
<path :style="cardinalTextRotateW"
class="c-cr__nsew-w --hide-small"
d="M56.7418 45.004H54.1378L52.7238 52.312H52.6958L51.2258 45.004H48.7758L47.3058 52.312H47.2778L45.8638 45.004H43.2598L45.9618 55H48.6078L49.9798 48.112H50.0078L51.3798 55H53.9838L56.7418 45.004Z"
/>
<path :style="cardinalTextRotateE"
class="c-cr__nsew-e --hide-small"
d="M46.104 55H54.21V52.76H48.708V50.856H53.608V48.84H48.708V47.09H54.07V45.004H46.104V55Z"
/>
<path :style="cardinalTextRotateS"
class="c-cr__nsew-s --hide-small"
d="M45.6531 51.64C45.6671 54.202 47.6971 55.21 49.9931 55.21C52.1911 55.21 54.3471 54.398 54.3471 51.864C54.3471 50.058 52.8911 49.386 51.4491 48.98C49.9931 48.574 48.5511 48.434 48.5511 47.664C48.5511 47.006 49.2511 46.81 49.8111 46.81C50.6091 46.81 51.4631 47.104 51.4211 48.014H54.0251C54.0111 45.76 52.0091 44.794 50.0211 44.794C48.1451 44.794 45.9471 45.648 45.9471 47.832C45.9471 49.666 47.4451 50.31 48.8731 50.716C50.3151 51.122 51.7431 51.29 51.7431 52.172C51.7431 52.914 50.9311 53.194 50.1471 53.194C49.0411 53.194 48.3131 52.816 48.2571 51.64H45.6531Z"
/>
<path :style="cardinalTextRotateN"
class="c-cr__nsew-n"
d="M42.5935 60H46.7935V49.32H46.8415L52.7935 60H57.3775V42.864H53.1775V53.424H53.1295L47.1775 42.864H42.5935V60Z"
/>
</g>
</g>
</g>
<defs>
<radialGradient id="paint0_radial"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(50 50) rotate(90) scale(50)"
>
<stop offset="0.751387"
stop-opacity="0"
/>
<stop offset="1"
stop-color="white"
/>
</radialGradient>
<radialGradient id="paint1_radial"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(50 -7) rotate(-90) scale(18.5)"
>
<stop offset="0.716377"
stop-color="#FFCC00"
/>
<stop offset="1"
stop-color="#FF9900"
stop-opacity="0"
/>
</radialGradient>
</defs>
</svg>
<text
class="c-nsew__label c-label-n"
text-anchor="middle"
:transform="northTextTransform"
>N</text>
<text
class="c-nsew__label c-label-e"
text-anchor="middle"
:transform="eastTextTransform"
>E</text>
<text
class="c-nsew__label c-label-w"
text-anchor="middle"
:transform="southTextTransform"
>W</text>
<text
class="c-nsew__label c-label-s"
text-anchor="middle"
:transform="westTextTransform"
>S</text>
</svg>
</div>
<div
v-if="hasHeading"
class="c-spacecraft-body"
:style="headingStyle"
>
</div>
<div
v-if="hasSunHeading"
class="c-sun"
:style="sunHeadingStyle"
></div>
<div
class="c-cam-field"
:style="cameraPanStyle"
>
<div class="cam-field-half cam-field-half-l">
<div
class="cam-field-area"
:style="cameraFOVStyleLeftHalf"
></div>
</div>
<div class="cam-field-half cam-field-half-r">
<div
class="cam-field-area"
:style="cameraFOVStyleRightHalf"
></div>
</div>
</div>
</div>
</div>
</template>
<script>
import { rotate } from './utils';
import { throttle } from 'lodash';
export default {
props: {
compassRoseSizingClasses: {
type: String,
sizedImageWidth: {
type: Number,
required: true
},
heading: {
type: Number,
required: true,
default() {
return 0;
}
required: true
},
sunHeading: {
type: Number,
@@ -229,39 +178,58 @@ export default {
},
cameraPan: {
type: Number,
required: true,
default() {
return 0;
}
required: true
},
sizedImageDimensions: {
type: Object,
lockCompass: {
type: Boolean,
required: true
}
},
data() {
return {
lockCompass: true
};
},
computed: {
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageWidth < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageWidth < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageWidth > 1000) {
compassRoseSizingClasses = '--rose-max';
}
return compassRoseSizingClasses;
},
compassRoseStyle() {
return { transform: `rotate(${ this.north }deg)` };
},
north() {
return this.lockCompass ? rotate(-this.cameraPan) : 0;
},
cardinalTextRotateN() {
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
northTextTransform() {
return this.cardinalPointsTextTransform.north;
},
cardinalTextRotateS() {
return { transform: `translateY(30%) rotate(${ -this.north }deg)` };
eastTextTransform() {
return this.cardinalPointsTextTransform.east;
},
cardinalTextRotateE() {
return { transform: `translateX(30%) rotate(${ -this.north }deg)` };
southTextTransform() {
return this.cardinalPointsTextTransform.south;
},
cardinalTextRotateW() {
return { transform: `translateX(-30%) rotate(${ -this.north }deg)` };
westTextTransform() {
return this.cardinalPointsTextTransform.west;
},
cardinalPointsTextTransform() {
/**
* cardinal points text must be rotated
* in the opposite direction that north is rotated
* to keep text vertically oriented
*/
const rotation = `rotate(${ -this.north })`;
return {
north: `translate(50,23) ${ rotation }`,
east: `translate(82,50) ${ rotation }`,
south: `translate(18,50) ${ rotation }`,
west: `translate(50,82) ${ rotation }`
};
},
hasHeading() {
return this.heading !== undefined;
@@ -270,7 +238,7 @@ export default {
const rotation = rotate(this.north, this.heading);
return {
transform: `rotate(${ rotation }deg)`
transform: `translateX(-50%) rotate(${ rotation }deg)`
};
},
hasSunHeading() {
@@ -294,37 +262,20 @@ export default {
// rotated counter-clockwise from camera pan angle
cameraFOVStyleLeftHalf() {
return {
transform: `rotate(${ this.cameraAngleOfView / 2 }deg)`
transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)`
};
},
// right half of camera field of view
// rotated clockwise from camera pan angle
cameraFOVStyleRightHalf() {
return {
transform: `rotate(${ -this.cameraAngleOfView / 2 }deg)`
transform: `translateX(-50%) rotate(${ this.cameraAngleOfView / 2 }deg)`
};
}
},
watch: {
sizedImageDimensions() {
this.debounceResizeSvg();
}
},
mounted() {
this.debounceResizeSvg = throttle(this.resizeSvg, 100);
this.$nextTick(() => {
this.debounceResizeSvg();
});
},
methods: {
resizeSvg() {
const svg = this.$refs.compassRoseSvg;
svg.setAttribute('width', this.$refs.compassRoseWrapper.clientWidth);
svg.setAttribute('height', this.$refs.compassRoseWrapper.clientHeight);
},
toggleLockCompass() {
this.lockCompass = !this.lockCompass;
this.$emit('toggle-lock-compass');
}
}
};

View File

@@ -12,8 +12,9 @@ $elemBg: rgba(black, 0.7);
.c-compass {
pointer-events: none; // This allows the image element to receive a browser-level context click
position: absolute;
left: 0;
top: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 1;
@include userSelectNone;
}
@@ -80,55 +81,114 @@ $elemBg: rgba(black, 0.7);
transform: translateX(-50%);
z-index: 1;
}
}
/***************************** COMPASS SVG */
.c-compass-rose-svg {
/***************************** COMPASS DIRECTIONS */
.c-nsew {
$color: $interfaceKeyColor;
position: absolute;
top: 0; left: 0;
$inset: 5%;
$tickHeightPerc: 15%;
text-shadow: black 0 0 10px;
top: $inset;
right: $inset;
bottom: $inset;
left: $inset;
z-index: 3;
g, path, rect {
// In an SVG, rotation occurs about the center of the SVG, not the element
transform-origin: center;
&__tick,
&__label {
fill: $color;
}
.c-cr {
&__bg {
fill: #000;
opacity: 0.8;
&__minor-ticks {
opacity: 0.5;
transform-origin: center;
transform: rotate(45deg);
}
&__label {
dominant-baseline: central;
font-size: 1.25em;
font-weight: bold;
}
.c-label-n {
font-size: 2em;
}
}
/***************************** CAMERA FIELD ANGLE */
.c-cam-field {
$color: white;
opacity: 0.3;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
.cam-field-half {
top: 0;
right: 0;
bottom: 0;
left: 0;
.cam-field-area {
background: $color;
top: -30%;
right: 0;
bottom: -30%;
left: 0;
}
&__edge {
opacity: 0.1;
// clip-paths overlap a bit to avoid a gap between halves
&-l {
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
.cam-field-area {
transform-origin: left center;
}
}
&__sun {
opacity: 0.7;
}
&-r {
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
&__cam-fov-l,
&__cam-fov-r {
// Cam FOV indication
opacity: 0.2;
fill: #fff;
.cam-field-area {
transform-origin: right center;
}
}
}
}
&__nsew-text,
&__spacecraft-body,
&__ticks-major,
&__ticks-minor {
fill: $color;
}
/***************************** SPACECRAFT BODY */
.c-spacecraft-body {
$color: $interfaceKeyColor;
$s: 30%;
background: $color;
border-radius: 3px;
height: $s;
width: $s;
left: 50%;
top: 50%;
opacity: 0.4;
transform-origin: center top;
transform: translateX(-50%); // center by default, overridden by CompassRose.vue / headingStyle()
&__ticks-minor {
opacity: 0.5;
transform: rotate(45deg);
}
&__spacecraft-body {
opacity: 0.3;
}
&:before {
// Direction arrow
$color: rgba(black, 0.5);
$arwPointerY: 60%;
$arwBodyOffset: 25%;
background: $color;
content: '';
display: block;
position: absolute;
top: 10%;
right: 20%;
bottom: 50%;
left: 20%;
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
}
}
@@ -136,28 +196,32 @@ $elemBg: rgba(black, 0.7);
.w-direction-rose {
$s: 10%;
$m: 2%;
cursor: pointer;
pointer-events: all;
position: absolute;
bottom: $m;
left: $m;
width: $s;
padding-top: $s;
z-index: 2;
&.--rose-min {
$s: 30px;
width: $s;
padding-top: $s;
.--hide-min {
display: none;
}
}
&.--rose-small {
.--hide-small {
.c-nsew__minor-ticks,
.c-tick-w,
.c-tick-s,
.c-tick-e,
.c-label-w,
.c-label-s,
.c-label-e {
display: none;
}
.c-label-n {
font-size: 2.5em;
}
}
&.--rose-max {
@@ -166,3 +230,44 @@ $elemBg: rgba(black, 0.7);
padding-top: $s;
}
}
.c-direction-rose {
$c2: rgba(white, 0.1);
background: $elemBg;
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
transform-origin: 0 0;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
clip-path: circle(50% at 50% 50%);
border-radius: 100%;
pointer-events: all;
svg, div {
position: absolute;
}
// Sun
.c-sun {
top: 0;
right: 0;
bottom: 0;
left: 0;
&:before {
$s: 35%;
@include sun();
content: '';
display: block;
position: absolute;
opacity: 0.7;
top: 0;
left: 50%;
height: $s;
width: $s;
transform: translate(-50%, -60%);
}
}
}

View File

@@ -55,34 +55,28 @@
></a>
</span>
</div>
<div ref="imageBG"
class="c-imagery__main-image__bg"
<div class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }"
@click="expand"
>
<div class="image-wrapper"
:style="{
'width': `${sizedImageDimensions.width}px`,
'height': `${sizedImageDimensions.height}px`
}"
<img
ref="focusedImage"
class="c-imagery__main-image__image js-imageryView-image"
:src="imageUrl"
:style="{
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
}"
:data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString"
>
<img ref="focusedImage"
class="c-imagery__main-image__image js-imageryView-image"
:src="imageUrl"
:style="{
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
}"
:data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString"
>
<Compass
v-if="shouldDisplayCompass"
:compass-rose-sizing-classes="compassRoseSizingClasses"
:image="focusedImage"
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
:sized-image-dimensions="sizedImageDimensions"
/>
</div>
<Compass
v-if="shouldDisplayCompass"
:container-width="imageContainerWidth"
:container-height="imageContainerHeight"
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
:image="focusedImage"
:lock-compass="lockCompass"
@toggle-lock-compass="toggleLockCompass"
/>
</div>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev"
@@ -130,40 +124,27 @@
</div>
</div>
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
:class="{'is-paused': isPaused}"
@scroll="handleScroll"
>
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-scroll-area"
@scroll="handleScroll"
<div v-for="(image, index) in imageHistory"
:key="image.url + image.time"
class="c-imagery__thumb c-thumb"
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)"
>
<div v-for="(image, index) in imageHistory"
:key="image.url + image.time"
class="c-imagery__thumb c-thumb"
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)"
<a href=""
:download="image.imageDownloadName"
@click.prevent
>
<a href=""
:download="image.imageDownloadName"
@click.prevent
<img class="c-thumb__image"
:src="image.url"
>
<img class="c-thumb__image"
:src="image.url"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
<button
class="c-imagery__auto-scroll-resume-button c-icon-button icon-play"
title="Resume automatic scrolling of image thumbnails"
@click="scrollToRight('reset')"
></button>
</div>
</div>
</template>
@@ -171,9 +152,8 @@
<script>
import _ from 'lodash';
import moment from 'moment';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
const DEFAULT_DURATION_FORMATTER = 'duration';
const REFRESH_CSS_MS = 500;
@@ -191,13 +171,11 @@ const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
const ARROW_RIGHT = 39;
const ARROW_LEFT = 37;
const SCROLL_LATENCY = 250;
export default {
components: {
Compass
},
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
inject: ['openmct', 'domainObject'],
data() {
let timeSystem = this.openmct.time.timeSystem();
@@ -226,23 +204,10 @@ export default {
focusedImageNaturalAspectRatio: undefined,
imageContainerWidth: undefined,
imageContainerHeight: undefined,
lockCompass: true,
resizingWindow: false
lockCompass: true
};
},
computed: {
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageDimensions.width < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageDimensions.width < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageDimensions.width > 1000) {
compassRoseSizingClasses = '--rose-max';
}
return compassRoseSizingClasses;
},
time() {
return this.formatTime(this.focusedImage);
},
@@ -366,20 +331,6 @@ export default {
}
return isFresh;
},
sizedImageDimensions() {
let sizedImageDimensions = {};
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
// container is wider than image
sizedImageDimensions.width = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
sizedImageDimensions.height = this.imageContainerHeight;
} else {
// container is taller than image
sizedImageDimensions.width = this.imageContainerWidth;
sizedImageDimensions.height = this.imageContainerWidth * this.focusedImageNaturalAspectRatio;
}
return sizedImageDimensions;
}
},
watch: {
@@ -428,14 +379,10 @@ export default {
_.debounce(this.resizeImageContainer, 400);
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
// For adjusting scroll bar size and position when resizing thumbs wrapper
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
this.imageContainerResizeObserver.observe(this.$refs.focusedImage);
},
updated() {
this.scrollToRight();
},
beforeDestroy() {
if (this.unsubscribe) {
@@ -447,10 +394,6 @@ export default {
this.imageContainerResizeObserver.disconnect();
}
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
}
@@ -470,16 +413,6 @@ export default {
}
},
methods: {
expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions();
const viewLargeAction = visibleActions
&& visibleActions.find(action => action.key === 'large.view');
if (viewLargeAction && viewLargeAction.appliesTo(this.objectPath, this.currentView)) {
viewLargeAction.onItemClicked();
}
},
async initializeRelatedTelemetry() {
this.relatedTelemetry = new RelatedTelemetry(
this.openmct,
@@ -628,15 +561,17 @@ export default {
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper || this.resizingWindow) {
if (!thumbsWrapper) {
return;
}
const { scrollLeft, scrollWidth, clientWidth } = thumbsWrapper;
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
const { scrollLeft, scrollWidth, clientWidth, scrollTop, scrollHeight, clientHeight } = thumbsWrapper;
const disableScroll = (scrollWidth - scrollLeft) > 2 * clientWidth
|| (scrollHeight - scrollTop) > 2 * clientHeight;
this.autoScroll = !disableScroll;
},
paused(state, type) {
this.isPaused = state;
if (type === 'button') {
@@ -649,7 +584,6 @@ export default {
}
this.autoScroll = true;
this.scrollToRight();
},
scrollToFocused() {
const thumbsWrapper = this.$refs.thumbsWrapper;
@@ -666,8 +600,8 @@ export default {
});
}
},
scrollToRight(type) {
if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
scrollToRight() {
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
return;
}
@@ -676,9 +610,7 @@ export default {
return;
}
this.$nextTick(() => {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
},
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
@@ -746,9 +678,9 @@ export default {
image.imageDownloadName = this.getImageDownloadName(datum);
this.imageHistory.push(image);
if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1);
this.scrollToRight();
}
},
getFormatter(key) {
@@ -876,31 +808,16 @@ export default {
}, { once: true });
},
resizeImageContainer() {
if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.imageBG.clientWidth;
if (this.$refs.focusedImage.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.focusedImage.clientWidth;
}
if (this.$refs.imageBG.clientHeight !== this.imageContainerHeight) {
this.imageContainerHeight = this.$refs.imageBG.clientHeight;
if (this.$refs.focusedImage.clientHeight !== this.imageContainerHeight) {
this.imageContainerHeight = this.$refs.focusedImage.clientHeight;
}
},
handleThumbWindowResizeStart() {
if (!this.autoScroll) {
return;
}
// To hide resume button while scrolling
this.resizingWindow = true;
this.handleThumbWindowResizeEnded();
},
handleThumbWindowResizeEnded() {
if (!this.isPaused) {
this.scrollToRight('reset');
}
this.$nextTick(() => {
this.resizingWindow = false;
});
toggleLockCompass() {
this.lockCompass = !this.lockCompass;
}
}
};

View File

@@ -1,7 +1,7 @@
.c-imagery {
display: flex;
flex-direction: column;
height: 100%;
flex: 1 1 auto;
overflow: hidden;
&:focus {
@@ -22,9 +22,6 @@
&__bg {
background-color: $colorPlotBg;
border: 1px solid transparent;
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 auto;
height: 0;
@@ -36,6 +33,7 @@
&__image {
height: 100%;
width: 100%;
object-fit: contain;
}
}
@@ -95,43 +93,24 @@
}
&__thumbs-wrapper {
display: flex; // Uses row layout
&.is-autoscroll-off {
background: $colorInteriorBorder;
[class*='__auto-scroll-resume-button'] {
display: block;
}
}
&.is-paused {
background: rgba($colorPausedBg, 0.4);
}
}
&__thumbs-scroll-area {
flex: 0 1 auto;
flex: 0 0 auto;
display: flex;
flex-direction: row;
height: 135px;
overflow-x: auto;
overflow-y: hidden;
margin-bottom: 1px;
padding-bottom: $interiorMarginSm;
&.is-paused {
background: rgba($colorPausedBg, 0.4);
}
.c-thumb:last-child {
// Hilite the lastest thumb
background: $colorBodyFg;
color: $colorBodyBg;
}
}
&__auto-scroll-resume-button {
display: none; // Set to block when __thumbs-wrapper has .is-autoscroll-off
flex: 0 0 auto;
font-size: 0.8em;
margin: $interiorMarginSm;
}
}
/*************************************** THUMBS */
@@ -163,7 +142,7 @@
.l-layout,
.c-fl {
.c-imagery__thumbs-scroll-area {
.c-imagery__thumbs-wrapper {
// When Imagery is in a layout, hide the thumbs area
display: none;
}
@@ -194,10 +173,6 @@
margin-right: $interiorMarginSm;
}
}
.s-status-taking-snapshot & {
display: none;
}
}
&__lc {
@@ -279,10 +254,6 @@
content: $glyph-icon-play;
}
}
.s-status-taking-snapshot & {
display: none;
}
}
.c-imagery__prev-next-buttons {
@@ -297,10 +268,6 @@
.c-nav {
pointer-events: all;
}
.s-status-taking-snapshot & {
display: none;
}
}
.c-nav {

View File

@@ -92,7 +92,6 @@ describe("The Imagery View Layout", () => {
let resolveFunction;
let openmct;
let appHolder;
let parent;
let child;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
@@ -196,7 +195,7 @@ describe("The Imagery View Layout", () => {
// this setups up the app
beforeEach((done) => {
appHolder = document.createElement('div');
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
@@ -210,8 +209,6 @@ describe("The Imagery View Layout", () => {
child = document.createElement('div');
parent.appendChild(child);
// document.querySelector('body').append(parent);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
disconnect() {}
@@ -280,7 +277,7 @@ describe("The Imagery View Layout", () => {
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
xit("should show the clicked thumbnail as the main image", (done) => {
it("should show the clicked thumbnail as the main image", (done) => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
@@ -317,7 +314,7 @@ describe("The Imagery View Layout", () => {
});
});
xit("should navigate via arrow keys", (done) => {
it("should navigate via arrow keys", (done) => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
@@ -365,21 +362,5 @@ describe("The Imagery View Layout", () => {
done();
});
});
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
await Vue.nextTick();
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryLayout, 'scrollToRight');
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset');
});
});
});

View File

@@ -30,6 +30,10 @@ describe('the plugin', function () {
const TEST_NAMESPACE = 'test';
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
openmct.install(new InterceptorPlugin(openmct));
@@ -42,7 +46,7 @@ describe('the plugin', function () {
element.appendChild(child);
openmct.on('start', done);
openmct.startHeadless();
openmct.startHeadless(appHolder);
});
afterEach(() => {
@@ -51,7 +55,6 @@ describe('the plugin', function () {
describe('the missingObjectInterceptor', () => {
let mockProvider;
beforeEach(() => {
mockProvider = jasmine.createSpyObj("mock provider", [
"get"
@@ -60,28 +63,27 @@ describe('the plugin', function () {
openmct.objects.addProvider(TEST_NAMESPACE, mockProvider);
});
it('returns missing objects', () => {
it('returns missing objects', (done) => {
const identifier = {
namespace: TEST_NAMESPACE,
key: 'hello'
};
return openmct.objects.get(identifier).then((testObject) => {
openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({
identifier,
type: 'unknown',
name: 'Missing: test:hello'
});
done();
});
});
it('returns the My items object if not found', () => {
it('returns the My items object if not found', (done) => {
const identifier = {
namespace: TEST_NAMESPACE,
key: 'mine'
};
return openmct.objects.get(identifier).then((testObject) => {
openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({
identifier,
"name": "My Items",
@@ -89,6 +91,7 @@ describe('the plugin', function () {
"composition": [],
"location": "ROOT"
});
done();
});
});

View File

@@ -37,14 +37,7 @@ export default class MoveAction {
let oldParent = objectPath[1];
let dialogService = this.openmct.$injector.get('dialogService');
let dialogForm = this.getDialogForm(object, oldParent);
let userInput;
try {
userInput = await dialogService.getUserInput(dialogForm, { name: object.name });
} catch (err) {
// user canceled, most likely
return;
}
let userInput = await dialogService.getUserInput(dialogForm, { name: object.name });
// if we need to update name
if (object.name !== userInput.name) {
@@ -111,13 +104,13 @@ export default class MoveAction {
{
key: "name",
control: "textfield",
name: "Name",
name: "Folder Name",
pattern: "\\S+",
required: true,
cssClass: "l-input-lg"
},
{
name: "Location",
name: "location",
control: "locator",
validate: this.validate(object, parent),
key: 'location'

View File

@@ -19,6 +19,8 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import MoveActionPlugin from './plugin.js';
import MoveAction from './MoveAction.js';
import {
createOpenMct,
resetApplicationState,
@@ -35,6 +37,10 @@ describe("The Move Action plugin", () => {
// this setups up the app
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
childObject = getMockObjects({
@@ -67,10 +73,11 @@ describe("The Move Action plugin", () => {
}
}).folder;
openmct.on('start', done);
openmct.startHeadless();
// already installed by default, but never hurts, just adds to context menu
openmct.install(MoveActionPlugin());
moveAction = openmct.actions._allActions.move;
openmct.on('start', done);
openmct.startHeadless(appHolder);
});
afterEach(() => {
@@ -78,12 +85,13 @@ describe("The Move Action plugin", () => {
});
it("should be defined", () => {
expect(moveAction).toBeDefined();
expect(MoveActionPlugin).toBeDefined();
});
describe("when moving an object to a new parent and removing from the old parent", () => {
beforeEach(() => {
moveAction = new MoveAction(openmct);
moveAction.addToNewParent(childObject, anotherParentObject);
moveAction.removeFromOldParent(parentObject, childObject);
});

View File

@@ -79,7 +79,7 @@ describe("the plugin", () => {
spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
return newFolderAction.invoke(mockObjectPath);
newFolderAction.invoke(mockObjectPath);
});
it('gets user input for folder name', () => {

View File

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

View File

@@ -31,7 +31,7 @@
</div>
<SearchResults v-if="search.length"
ref="searchResults"
:domain-object="domainObject"
:domain-object="internalDomainObject"
:results="searchResults"
@changeSectionPage="changeSelectedSection"
@updateEntries="updateEntries"
@@ -43,20 +43,15 @@
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:default-page-id="defaultPageId"
:selected-page-id="getSelectedPageId()"
:default-section-id="defaultSectionId"
:selected-section-id="getSelectedSectionId()"
:domain-object="domainObject"
:page-title="domainObject.configuration.pageTitle"
:section-title="domainObject.configuration.sectionTitle"
:domain-object="internalDomainObject"
:page-title="internalDomainObject.configuration.pageTitle"
:section-title="internalDomainObject.configuration.sectionTitle"
:sections="sections"
:selected-section="selectedSection"
:sidebar-covers-entries="sidebarCoversEntries"
@defaultPageDeleted="cleanupDefaultNotebook"
@defaultSectionDeleted="cleanupDefaultNotebook"
@pagesChanged="pagesChanged"
@selectPage="selectPage"
@sectionsChanged="sectionsChanged"
@selectSection="selectSection"
@toggleNav="toggleNav"
/>
<div class="c-notebook__page-view">
@@ -66,10 +61,10 @@
></button>
<div class="c-notebook__page-view__path c-path">
<span class="c-notebook__path__section c-path__item">
{{ selectedSection ? selectedSection.name : '' }}
{{ getSelectedSection() ? getSelectedSection().name : '' }}
</span>
<span class="c-notebook__path__page c-path__item">
{{ selectedPage ? selectedPage.name : '' }}
{{ getSelectedPage() ? getSelectedPage().name : '' }}
</span>
</div>
<div class="c-notebook__page-view__controls">
@@ -120,9 +115,9 @@
<NotebookEntry v-for="entry in filteredAndSortedEntries"
:key="entry.id"
:entry="entry"
:domain-object="domainObject"
:selected-page="selectedPage"
:selected-section="selectedSection"
:domain-object="internalDomainObject"
:selected-page="getSelectedPage()"
:selected-section="getSelectedSection()"
:read-only="false"
@deleteEntry="deleteEntry"
@updateEntry="updateEntry"
@@ -138,7 +133,7 @@ import NotebookEntry from './NotebookEntry.vue';
import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
import objectUtils from 'objectUtils';
@@ -157,21 +152,14 @@ export default {
SearchResults,
Sidebar
},
inject: ['openmct', 'snapshotContainer'],
props: {
domainObject: {
type: Object,
required: true
}
},
inject: ['openmct', 'domainObject', 'snapshotContainer'],
data() {
return {
defaultPageId: this.getDefaultPageId(),
defaultSectionId: this.getDefaultSectionId(),
selectedSectionId: this.getSelectedSectionId(),
selectedPageId: this.getSelectedPageId(),
defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '',
defaultSectionId: getDefaultNotebook() ? getDefaultNotebook().section.id : '',
defaultSort: this.domainObject.configuration.defaultSort,
focusEntryId: null,
internalDomainObject: this.domainObject,
search: '',
searchResults: [],
showTime: 0,
@@ -182,7 +170,7 @@ export default {
computed: {
filteredAndSortedEntries() {
const filterTime = Date.now();
const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
const hours = parseInt(this.showTime, 10);
const filteredPageEntriesByTime = hours
@@ -197,42 +185,22 @@ export default {
return this.getPages() || [];
},
sections() {
return this.getSections();
return this.internalDomainObject.configuration.sections || [];
},
selectedPage() {
const pages = this.getPages();
if (!pages.length) {
return undefined;
if (!pages) {
return {};
}
const selectedPage = pages.find(page => page.id === this.selectedPageId);
if (selectedPage) {
return selectedPage;
}
const defaultPage = pages.find(page => page.id === this.defaultPageId);
if (defaultPage) {
return defaultPage;
}
return this.pages[0];
return pages.find(page => page.isSelected);
},
selectedSection() {
if (!this.sections.length) {
return undefined;
return {};
}
const selectedSection = this.sections.find(section => section.id === this.selectedSectionId);
if (selectedSection) {
return selectedSection;
}
const defaultSection = this.sections.find(section => section.id === this.defaultSectionId);
if (defaultSection) {
return defaultSection;
}
return this.sections[0];
return this.sections.find(section => section.isSelected);
}
},
watch: {
@@ -242,14 +210,16 @@ export default {
},
beforeMount() {
this.getSearchResults = debounce(this.getSearchResults, 500);
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.formatSidebar();
this.setSectionAndPageFromUrl();
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
window.addEventListener("hashchange", this.navigateToSectionPage, false);
this.openmct.router.on('change:params', this.changeSectionPage);
this.navigateToSectionPage();
},
beforeDestroy() {
if (this.unlisten) {
@@ -257,7 +227,8 @@ export default {
}
window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
window.removeEventListener("hashchange", this.navigateToSectionPage);
this.openmct.router.off('change:params', this.changeSectionPage);
},
updated: function () {
this.$nextTick(() => {
@@ -313,33 +284,24 @@ export default {
this.sectionsChanged({ sections });
this.resetSearch();
},
cleanupDefaultNotebook() {
this.defaultPageId = undefined;
this.defaultSectionId = undefined;
this.removeDefaultClass(this.domainObject);
clearDefaultNotebook();
},
setSectionAndPageFromUrl() {
let sectionId = this.getSectionIdFromUrl() || this.getDefaultSectionId() || this.getSelectedSectionId();
let pageId = this.getPageIdFromUrl() || this.getDefaultPageId() || this.getSelectedPageId();
this.selectSection(sectionId);
this.selectPage(pageId);
},
createNotebookStorageObject() {
const page = this.selectedPage;
const section = this.selectedSection;
const notebookMeta = {
name: this.internalDomainObject.name,
identifier: this.internalDomainObject.identifier,
link: this.getLinktoNotebook()
};
const page = this.getSelectedPage();
const section = this.getSelectedSection();
return {
name: this.domainObject.name,
identifier: this.domainObject.identifier,
link: this.getLinktoNotebook(),
defaultSectionId: section.id,
defaultPageId: page.id
notebookMeta,
page,
section
};
},
deleteEntry(entryId) {
const entryPos = getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
const self = this;
const entryPos = getEntryPosById(entryId, this.internalDomainObject, this.selectedSection, this.selectedPage);
if (entryPos === -1) {
this.openmct.notifications.alert('Warning: unable to delete entry');
console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`);
@@ -355,9 +317,9 @@ export default {
label: "Ok",
emphasis: true,
callback: () => {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const entries = getNotebookEntries(self.internalDomainObject, self.selectedSection, self.selectedPage);
entries.splice(entryPos, 1);
this.updateEntries(entries);
self.updateEntries(entries);
dialog.dismiss();
}
},
@@ -433,30 +395,13 @@ export default {
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
this.sidebarCoversEntries = sidebarCoversEntries;
},
getDefaultPageId() {
return this.isDefaultNotebook()
? getDefaultNotebook().defaultPageId
: undefined;
},
isDefaultNotebook() {
const defaultNotebook = getDefaultNotebook();
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.identifier;
return defaultNotebookIdentifier !== null
&& this.openmct.objects.areIdsEqual(defaultNotebookIdentifier, this.domainObject.identifier);
},
getDefaultSectionId() {
return this.isDefaultNotebook()
? getDefaultNotebook().defaultSectionId
: undefined;
},
getDefaultNotebookObject() {
const oldNotebookStorage = getDefaultNotebook();
if (!oldNotebookStorage) {
return null;
}
return this.openmct.objects.get(oldNotebookStorage.identifier);
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier);
},
getLinktoNotebook() {
const objectPath = this.openmct.router.path;
@@ -478,17 +423,14 @@ export default {
getSection(id) {
return this.sections.find(s => s.id === id);
},
getSections() {
return this.domainObject.configuration.sections || [];
},
getSearchResults() {
if (!this.search.length) {
return [];
}
const output = [];
const sections = this.domainObject.configuration.sections;
const entries = this.domainObject.configuration.entries;
const sections = this.internalDomainObject.configuration.sections;
const entries = this.internalDomainObject.configuration.entries;
const searchTextLower = this.search.toLowerCase();
const originalSearchText = this.search;
let sectionTrackPageHit;
@@ -567,41 +509,77 @@ export default {
this.searchResults = output;
},
getPages() {
const selectedSection = this.selectedSection;
const selectedSection = this.getSelectedSection();
if (!selectedSection || !selectedSection.pages.length) {
return [];
}
return selectedSection.pages;
},
getSelectedPageId() {
const page = this.selectedPage;
if (!page) {
return undefined;
getSelectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
}
return page.id;
const selectedPage = pages.find(page => page.isSelected);
if (selectedPage) {
return selectedPage;
}
if (!selectedPage && !pages.length) {
return null;
}
pages[0].isSelected = true;
return pages[0];
},
getSelectedSectionId() {
const section = this.selectedSection;
if (!section) {
return undefined;
getSelectedSection() {
if (!this.sections.length) {
return null;
}
return section.id;
return this.sections.find(section => section.isSelected);
},
navigateToSectionPage() {
let { pageId, sectionId } = this.openmct.router.getParams();
if (!pageId || !sectionId) {
sectionId = this.selectedSection.id;
pageId = this.selectedPage.id;
}
const sections = this.sections.map(s => {
s.isSelected = false;
if (s.id === sectionId) {
s.isSelected = true;
s.pages.forEach(p => p.isSelected = (p.id === pageId));
}
return s;
});
const selectedSectionId = this.selectedSection && this.selectedSection.id;
const selectedPageId = this.selectedPage && this.selectedPage.id;
if (selectedPageId === pageId && selectedSectionId === sectionId) {
return;
}
this.sectionsChanged({ sections });
},
newEntry(embed = null) {
this.resetSearch();
const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage);
const id = addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed);
this.focusEntryId = id;
},
orientationChange() {
this.formatSidebar();
},
pagesChanged({ pages = [], id = null}) {
const selectedSection = this.selectedSection;
const selectedSection = this.getSelectedSection();
if (!selectedSection) {
return;
}
@@ -616,6 +594,7 @@ export default {
});
this.sectionsChanged({ sections });
this.updateDefaultNotebookPage(pages, id);
},
removeDefaultClass(domainObject) {
if (!domainObject) {
@@ -633,25 +612,50 @@ export default {
},
async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
const isSameNotebook = defaultNotebookObject
&& objectUtils.makeKeyString(defaultNotebookObject.identifier) === objectUtils.makeKeyString(notebookStorage.identifier);
if (!isSameNotebook) {
if (!defaultNotebookObject) {
setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject);
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject);
}
if (!defaultNotebookObject || !isSameNotebook) {
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
this.defaultSectionId = notebookStorage.section.id;
setDefaultNotebookSection(notebookStorage.section);
}
if (this.defaultSectionId !== notebookStorage.defaultSectionId) {
setDefaultNotebookSectionId(notebookStorage.defaultSectionId);
this.defaultSectionId = notebookStorage.defaultSectionId;
if (this.defaultPageId && this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
this.defaultPageId = notebookStorage.page.id;
setDefaultNotebookPage(notebookStorage.page);
}
},
updateDefaultNotebookPage(pages, id) {
if (!id) {
return;
}
if (this.defaultPageId !== notebookStorage.defaultPageId) {
setDefaultNotebookPageId(notebookStorage.defaultPageId);
this.defaultPageId = notebookStorage.defaultPageId;
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
return;
}
const defaultNotebookPage = notebookStorage.page;
const page = pages.find(p => p.id === id);
if (!page && defaultNotebookPage.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null;
this.removeDefaultClass(this.internalDomainObject);
clearDefaultNotebook();
return;
}
if (id !== defaultNotebookPage.id) {
return;
}
setDefaultNotebookPage(page);
},
updateDefaultNotebookSection(sections, id) {
if (!id) {
@@ -660,76 +664,72 @@ export default {
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.identifier.key !== this.domainObject.identifier.key) {
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
return;
}
const defaultNotebookSectionId = notebookStorage.defaultSectionId;
if (defaultNotebookSectionId === id) {
const section = sections.find(s => s.id === id);
if (!section) {
this.removeDefaultClass(this.domainObject);
clearDefaultNotebook();
const defaultNotebookSection = notebookStorage.section;
const section = sections.find(s => s.id === id);
if (!section && defaultNotebookSection.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null;
this.removeDefaultClass(this.internalDomainObject);
clearDefaultNotebook();
return;
}
}
if (id !== defaultNotebookSectionId) {
return;
}
setDefaultNotebookSectionId(defaultNotebookSectionId);
if (id !== defaultNotebookSection.id) {
return;
}
setDefaultNotebookSection(section);
},
updateEntry(entry) {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
const entries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage);
const entryPos = getEntryPosById(entry.id, this.internalDomainObject, this.selectedSection, this.selectedPage);
entries[entryPos] = entry;
this.updateEntries(entries);
},
updateEntries(entries) {
const configuration = this.domainObject.configuration;
const configuration = this.internalDomainObject.configuration;
const notebookEntries = configuration.entries || {};
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries);
mutateObject(this.openmct, this.internalDomainObject, 'configuration.entries', notebookEntries);
},
getPageIdFromUrl() {
return this.openmct.router.getParams().pageId;
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
},
getSectionIdFromUrl() {
return this.openmct.router.getParams().sectionId;
},
syncUrlWithPageAndSection() {
updateParams(sections) {
const selectedSection = sections.find(s => s.isSelected);
if (!selectedSection) {
return;
}
const selectedPage = selectedSection.pages.find(p => p.isSelected);
if (!selectedPage) {
return;
}
const sectionId = selectedSection.id;
const pageId = selectedPage.id;
if (!sectionId || !pageId) {
return;
}
this.openmct.router.updateParams({
pageId: this.selectedPageId,
sectionId: this.selectedSectionId
sectionId,
pageId
});
},
sectionsChanged({ sections, id = undefined }) {
mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections);
sectionsChanged({ sections, id = null }) {
mutateObject(this.openmct, this.internalDomainObject, 'configuration.sections', sections);
this.updateParams(sections);
this.updateDefaultNotebookSection(sections, id);
},
selectPage(pageId) {
if (!pageId) {
return;
}
this.selectedPageId = pageId;
this.syncUrlWithPageAndSection();
},
selectSection(sectionId) {
if (!sectionId) {
return;
}
this.selectedSectionId = sectionId;
const pageId = this.selectedSection.pages[0].id;
this.selectPage(pageId);
this.syncUrlWithPageAndSection();
}
}
};

View File

@@ -4,7 +4,7 @@
class="c-ne__embed__snap-thumb"
@click="openSnapshot()"
>
<img :src="thumbnailImage">
<img :src="embed.snapshot.src">
</div>
<div class="c-ne__embed__info">
<div class="c-ne__embed__name">
@@ -25,15 +25,11 @@
<script>
import Moment from 'moment';
import PopupMenu from './PopupMenu.vue';
import PreviewAction from '../../../ui/preview/PreviewAction';
import RemoveDialog from '../utils/removeDialog';
import PainterroInstance from '../utils/painterroInstance';
import SnapshotTemplate from './snapshot-template.html';
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
import ImageExporter from '../../../exporters/ImageExporter';
import PopupMenu from './PopupMenu.vue';
import Vue from 'vue';
export default {
@@ -63,16 +59,11 @@ export default {
computed: {
createdOn() {
return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss');
},
thumbnailImage() {
return this.embed.snapshot.thumbnailImage
? this.embed.snapshot.thumbnailImage.src
: this.embed.snapshot.src;
}
},
mounted() {
this.addPopupMenuItems();
this.imageExporter = new ImageExporter(this.openmct);
this.exportImageService = this.openmct.$injector.get('exportImageService');
},
methods: {
addPopupMenuItems() {
@@ -94,7 +85,7 @@ export default {
template: '<div id="snap-annotation"></div>'
}).$mount();
const painterroInstance = new PainterroInstance(annotateVue.$el);
const painterroInstance = new PainterroInstance(annotateVue.$el, this.updateSnapshot);
const annotateOverlay = this.openmct.overlays.overlay({
element: annotateVue.$el,
size: 'large',
@@ -102,6 +93,7 @@ export default {
buttons: [
{
label: 'Cancel',
emphasis: true,
callback: () => {
painterroInstance.dismiss();
annotateOverlay.dismiss();
@@ -109,14 +101,11 @@ export default {
},
{
label: 'Save',
emphasis: true,
callback: () => {
painterroInstance.save((snapshotObject) => {
annotateOverlay.dismiss();
this.snapshotOverlay.dismiss();
this.updateSnapshot(snapshotObject);
this.openSnapshotOverlay(snapshotObject.fullSizeImage.src);
});
painterroInstance.save();
annotateOverlay.dismiss();
this.snapshotOverlay.dismiss();
this.openSnapshot();
}
}
],
@@ -126,19 +115,7 @@ export default {
});
painterroInstance.intialize();
const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;
if (!fullSizeImageObjectIdentifier) {
// legacy image data stored in embed
painterroInstance.show(this.embed.snapshot.src);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
painterroInstance.show(object.configuration.fullSizeImageURL);
});
painterroInstance.show(this.embed.snapshot.src);
},
changeLocation() {
const hash = this.embed.historicLink;
@@ -182,29 +159,12 @@ export default {
removeDialog.show();
},
openSnapshot() {
const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;
if (!fullSizeImageObjectIdentifier) {
// legacy image data stored in embed
this.openSnapshotOverlay(this.embed.snapshot.src);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
this.openSnapshotOverlay(object.configuration.fullSizeImageURL);
});
},
openSnapshotOverlay(src) {
const self = this;
this.snapshot = new Vue({
data: () => {
return {
createdOn: this.createdOn,
name: this.embed.name,
cssClass: this.embed.cssClass,
src
embed: this.embed
};
},
methods: {
@@ -235,9 +195,9 @@ export default {
let element = this.snapshot.$refs['snapshot-image'];
if (type === 'png') {
this.imageExporter.exportPNG(element, this.embed.name);
this.exportImageService.exportPNG(element, this.embed.name);
} else {
this.imageExporter.exportJPG(element, this.embed.name);
this.exportImageService.exportJPG(element, this.embed.name);
}
},
previewEmbed() {
@@ -257,9 +217,7 @@ export default {
this.$emit('updateEmbed', embed);
},
updateSnapshot(snapshotObject) {
this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
this.embed.snapshot = snapshotObject;
this.updateEmbed(this.embed);
}
}

View File

@@ -62,6 +62,7 @@
<NotebookEmbed v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
:entry="entry"
@removeEmbed="removeEmbed"
@updateEmbed="updateEmbed"
/>
@@ -253,7 +254,6 @@ export default {
},
removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
// TODO: remove notebook snapshot object using object remove API
this.entry.embeds.splice(embedPosition, 1);
this.$emit('updateEntry', this.entry);

View File

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

View File

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

View File

@@ -6,7 +6,6 @@
>
<Page ref="pageComponent"
:default-page-id="defaultPageId"
:selected-page-id="selectedPageId"
:page="page"
:page-title="pageTitle"
@deletePage="deletePage"
@@ -34,13 +33,11 @@ export default {
return '';
}
},
selectedPageId: {
type: String,
required: true
},
domainObject: {
type: Object,
required: true
default() {
return {};
}
},
pages: {
type: Array,
@@ -69,17 +66,7 @@ export default {
}
}
},
watch: {
pages() {
if (!this.containsPage(this.selectedPageId)) {
this.selectPage(this.pages[0].id);
}
}
},
methods: {
containsPage(pageId) {
return this.pages.some(page => page.id === pageId);
},
deletePage(id) {
const selectedSection = this.sections.find(s => s.isSelected);
const page = this.pages.find(p => p.id === id);
@@ -87,37 +74,41 @@ export default {
const selectedPage = this.pages.find(p => p.isSelected);
const defaultNotebook = getDefaultNotebook();
const defaultPageId = defaultNotebook && defaultNotebook.defaultPageId;
const defaultpage = defaultNotebook && defaultNotebook.page;
const isPageSelected = selectedPage && selectedPage.id === id;
const isPageDefault = defaultPageId === id;
const isPageDefault = defaultpage && defaultpage.id === id;
const pages = this.pages.filter(s => s.id !== id);
let selectedPageId;
if (isPageSelected && defaultPageId) {
if (isPageSelected && defaultpage) {
pages.forEach(s => {
s.isSelected = false;
if (defaultPageId === s.id) {
selectedPageId = s.id;
if (defaultpage && defaultpage.id === s.id) {
s.isSelected = true;
}
});
}
if (isPageDefault) {
this.$emit('defaultPageDeleted');
}
if (pages.length && isPageSelected && (!defaultPageId || isPageDefault)) {
selectedPageId = pages[0].id;
if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) {
pages[0].isSelected = true;
}
this.$emit('updatePage', {
pages,
id
});
this.$emit('selectPage', selectedPageId);
},
selectPage(id) {
this.$emit('selectPage', id);
const pages = this.pages.map(page => {
const isSelected = page.id === id;
page.isSelected = isSelected;
return page;
});
this.$emit('updatePage', {
pages,
id
});
// Add test here for whether or not to toggle the nav
if (this.sidebarCoversEntries) {

View File

@@ -1,6 +1,6 @@
<template>
<div class="c-list__item js-list__item"
:class="[{ 'is-selected': isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
:class="[{ 'is-selected': page.isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
:data-id="page.id"
@click="selectPage"
>
@@ -29,10 +29,6 @@ export default {
return '';
}
},
selectedPageId: {
type: String,
required: true
},
page: {
type: Object,
required: true
@@ -50,11 +46,6 @@ export default {
removeActionString: `Delete ${this.pageTitle}`
};
},
computed: {
isSelected() {
return this.selectedPageId === this.page.id;
}
},
watch: {
page(newPage) {
this.toggleContentEditable(newPage);
@@ -82,7 +73,7 @@ export default {
this.$emit('deletePage', this.page.id);
},
getRemoveDialog() {
const message = 'Other users may be editing entries in this page, and deleting it is permanent. Do you want to continue?';
const message = 'This action will delete this page and all of its entries. Do you want to continue?';
const options = {
name: this.removeActionString,
callback: this.deletePage.bind(this),

View File

@@ -4,14 +4,13 @@
:key="section.id"
class="c-list__item-h"
>
<NotebookSection ref="sectionComponent"
:default-section-id="defaultSectionId"
:selected-section-id="selectedSectionId"
:section="section"
:section-title="sectionTitle"
@deleteSection="deleteSection"
@renameSection="updateSection"
@selectSection="selectSection"
<sectionComponent ref="sectionComponent"
:default-section-id="defaultSectionId"
:section="section"
:section-title="sectionTitle"
@deleteSection="deleteSection"
@renameSection="updateSection"
@selectSection="selectSection"
/>
</li>
</ul>
@@ -20,11 +19,11 @@
<script>
import { deleteNotebookEntries } from '../utils/notebook-entries';
import { getDefaultNotebook } from '../utils/notebook-storage';
import SectionComponent from './SectionComponent.vue';
import sectionComponent from './SectionComponent.vue';
export default {
components: {
NotebookSection: SectionComponent
sectionComponent
},
inject: ['openmct'],
props: {
@@ -34,10 +33,6 @@ export default {
return '';
}
},
selectedSectionId: {
type: String,
required: true
},
domainObject: {
type: Object,
default() {
@@ -58,42 +53,28 @@ export default {
}
}
},
watch: {
sections() {
if (!this.containsSection(this.selectedSectionId)) {
this.selectSection(this.sections[0].id);
}
}
},
methods: {
containsSection(sectionId) {
return this.sections.some(section => section.id === sectionId);
},
deleteSection(id) {
const section = this.sections.find(s => s.id === id);
deleteNotebookEntries(this.openmct, this.domainObject, section);
const selectedSection = this.sections.find(s => s.id === this.selectedSectionId);
const selectedSection = this.sections.find(s => s.isSelected);
const defaultNotebook = getDefaultNotebook();
const defaultSectionId = defaultNotebook && defaultNotebook.defaultSectionId;
const defaultSection = defaultNotebook && defaultNotebook.section;
const isSectionSelected = selectedSection && selectedSection.id === id;
const isSectionDefault = defaultSectionId === id;
const isSectionDefault = defaultSection && defaultSection.id === id;
const sections = this.sections.filter(s => s.id !== id);
if (isSectionSelected && defaultSectionId) {
if (isSectionSelected && defaultSection) {
sections.forEach(s => {
s.isSelected = false;
if (defaultSectionId === s.id) {
if (defaultSection && defaultSection.id === s.id) {
s.isSelected = true;
}
});
}
if (isSectionDefault) {
this.$emit('defaultSectionDeleted');
}
if (sections.length && isSectionSelected && (!defaultSectionId || isSectionDefault)) {
if (sections.length && isSectionSelected && (!defaultSection || isSectionDefault)) {
sections[0].isSelected = true;
}
@@ -102,8 +83,18 @@ export default {
id
});
},
selectSection(id) {
this.$emit('selectSection', id);
selectSection(id, newSections) {
const currentSections = newSections || this.sections;
const sections = currentSections.map(section => {
const isSelected = section.id === id;
section.isSelected = isSelected;
return section;
});
this.$emit('updateSection', {
sections,
id
});
},
updateSection(newSection) {
const id = newSection.id;

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