Compare commits

...

15 Commits

Author SHA1 Message Date
Henry Hsu
2def6dd634 add mmgis dependency 2021-07-19 11:32:24 -07:00
David Tsay
16fe1b55c6 bubble activity logic to parent 2021-07-13 17:06:11 -07:00
David Tsay
8037c372b0 add missing copyrights 2021-07-13 16:33:26 -07:00
David Tsay
216a349282 allow custom details while retaining current domain object details 2021-07-13 16:32:22 -07:00
Charles Hacskaylo
41b860a547 Fixes for Imagery Snapshotting #3963 (#4001)
* refactored compass structure and code.
* resize image wrapper
* center image properly
* Refactor imagery compass rose as SVG
* Suppress prev/next image arrows from Snapshot

Co-authored-by: Nikhil Mandlik <nikhil.k.mandlik@nasa.gov>
2021-07-09 18:03:11 -07:00
Nikhil
254b3db966 added mock values to compass for example imagery. (#3999) 2021-07-09 15:13:54 -07:00
Shefali Joshi
cbb3f32d1e 1.7.4 into master (#3985)
catching any errors from a user canceling from dialog (#3968)
Fix navigation errors (#3970)
Only add listeners to observables on creation
Do not double destroy mutable objects
Disallow pause and play in time strip view for plots. (#3972)
Update version

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-29 09:40:30 -07:00
Henry Hsu
e3bf72e77f New time conductor popups should be closed by hitting ESC key (#3978) 2021-06-29 09:03:58 -07:00
Charles Hacskaylo
0b63b782cf Image thumbs autoscroll resumption functionality (#3892)
* add logs for testing

* check autoScroll value

* Add auto scroll button at thumbnails

* add auto scroll button functionality

* turn on auto scroll whenever scroll bar is not at the right end

* check if scroll right when the page load

* update scroll to right condition

* remove resetScroll method, refactor scrollToRight and some cleaning

* check initial status

* remove logs

* Styling for imagery thumbs 'autoscroll resume' button

- Refined look and feel of the approach;
- CSS classes now assigned at the wrapper level;
- Markup changed to allow wrapping of scroll area and button;
- TODO: prevent a drag resize of the main view area from forcing
autoscroll to pause;

* Add tests for auto scroll

* add resize observer for thumb wrapper

* reset scroll bar position when window is resized

* Revert "upgrade to webpack5 (#3871)" (#3907) (#3908)

This reverts commit e1e0eeac56.

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>

* Check for getlimits api (#3910)

* add debounce and resizing observer to thumbwrapper

* handling resizing window and auto scroll

* 1.clean up comments and logs 2.make variable names more descriptive 3.add scroll to right for play button

* fix eslint formate issue

* update class name in test

* fix eslint format error

* remove a couple variables that were created but not used.

Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
Co-authored-by: Henry Hsu <hhsu0219@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-29 07:52:27 -07:00
Charles Hacskaylo
da39fd0c70 Overlay fixes and improvements (#3901)
* Overlay-related fixes
- Prevent navigation when a folder's grid or list view is displayed in
an overlay;

* Overlay-related fixes
- Get rid of theme-based special overlay coloring that was making the
overlay hard to use;
- Add margin to `l-overlay-large` to make it clearer that user is in an
overlay when interacting with that view;
- Refinements to colors and layout in About screens;
2021-06-28 14:35:35 -07:00
Charles Hacskaylo
96dd581a67 Timestrip plan Inspection (#3863)
* Stub in static HTML for Timestrip Activity Inspection
- Added static markup with placeholder values and display logic;
- Refined approach for Links;
* Refactor duration formatting
* Display activity name when it's available
* Don't use indices for keys
* Don't show properties with no labels
2021-06-28 10:04:54 -07:00
Jamie V
2a1e322230 adding link to discussions in readme to encourage users to showcase their use of Open MCT (#3933)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-06-22 09:34:36 -07:00
Shefali Joshi
300b98bd54 Disallow pan, zoom and pause/play controls in time strip view (#3936)
* Disallow pan and zoom when in time strip view

* Disable plot controls in time strip view
2021-06-22 09:25:12 -07:00
Andrew Henry
c946609d13 Added LGTM code quality badge (#3960)
We score an A, we should flaunt it! (We should also aim for A+).
2021-06-22 09:10:08 -07:00
David Tsay
7ca559fbe4 [Styles] add unit tests (#3557)
* unit tests for inspector styles feature
* add mock capability for local storage

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-22 06:50:49 -07:00
48 changed files with 2005 additions and 637 deletions

View File

@@ -1,9 +1,11 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0)
# 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 (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

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

View File

@@ -1,8 +1,10 @@
{
"name": "openmct",
"version": "1.7.4-SNAPSHOT",
"version": "1.7.4",
"description": "The Open MCT core platform",
"dependencies": {},
"dependencies": {
"openmct-mmgis": "git+https://trunk.arc.nasa.gov/bitbucket/scm/vipergds/openmct-mmgis.git#api-mmgis-inspector"
},
"devDependencies": {
"angular": ">=1.8.0",
"angular-route": "1.4.14",
@@ -34,7 +36,7 @@
"git-rev-sync": "^1.4.0",
"glob": ">= 3.0.0",
"html-loader": "^0.5.5",
"html2canvas": "^1.0.0-alpha.12",
"html2canvas": "^1.0.0-rc.7",
"imports-loader": "^0.8.0",
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.1.0",

View File

@@ -399,25 +399,25 @@ 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
&& 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();
});
}
}
return mutableObject;

View File

@@ -21,8 +21,7 @@
&__outer {
@include abs();
background: $overlayColorBg;
color: $overlayColorFg;
background: $colorBodyBg;
display: flex;
flex-direction: column;
padding: $overlayInnerMargin;
@@ -30,7 +29,6 @@
&__close-button {
$p: $interiorMargin + 2px;
color: $overlayColorFg;
font-size: 1.5em;
position: absolute;
top: $p; right: $p;
@@ -82,11 +80,6 @@
}
}
.c-button,
.c-click-icon {
filter: $overlayBrightnessAdjust;
}
.c-object-label__name {
filter: $objectLabelNameFilter;
}
@@ -103,6 +96,7 @@ 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 {
@@ -124,12 +118,8 @@ body.desktop {
$tbPad: floor($pad * 0.8);
$lrPad: $pad;
.c-overlay {
&__blocker {
display: none;
}
&__outer {
@include overlaySizing($overlayOuterMarginFullscreen);
@include overlaySizing($overlayOuterMarginLarge);
padding: $tbPad $lrPad;
}

View File

@@ -37,7 +37,15 @@ export default class DuplicateAction {
let duplicationTask = new DuplicateTask(this.openmct);
let originalObject = objectPath[0];
let parent = objectPath[1];
let userInput = await this.getUserInput(originalObject, parent);
let userInput;
try {
userInput = await this.getUserInput(originalObject, parent);
} catch (error) {
// user most likely canceled
return;
}
let newParent = userInput.location;
let inNavigationPath = this.inNavigationPath(originalObject);

View File

@@ -23,6 +23,11 @@
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,4 +22,9 @@
@include isAlias();
}
}
[class*='l-overlay'] & {
// When this view is in an overlay, prevent navigation
pointer-events: none;
}
}

View File

@@ -62,6 +62,9 @@ export default function ImageryViewProvider(openmct) {
destroy: function () {
component.$destroy();
component = undefined;
},
_getInstance: function () {
return component;
}
};
}

View File

@@ -23,7 +23,7 @@
<template>
<div
class="c-compass"
:style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`"
:style="`width: 100%; height: 100%`"
>
<CompassHUD
v-if="hasCameraFieldOfView"
@@ -33,13 +33,12 @@
/>
<CompassRose
v-if="hasCameraFieldOfView"
:heading="heading"
:sized-image-width="sizedImageDimensions.width"
:sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan"
:lock-compass="lockCompass"
@toggle-lock-compass="toggleLockCompass"
:compass-rose-sizing-classes="compassRoseSizingClasses"
:heading="heading"
:sized-image-dimensions="sizedImageDimensions"
:sun-heading="sunHeading"
/>
</div>
</template>
@@ -56,42 +55,20 @@ export default {
CompassRose
},
props: {
containerWidth: {
type: Number,
required: true
},
containerHeight: {
type: Number,
required: true
},
naturalAspectRatio: {
type: Number,
compassRoseSizingClasses: {
type: String,
required: true
},
image: {
type: Object,
required: true
},
lockCompass: {
type: Boolean,
sizedImageDimensions: {
type: Object,
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,152 +21,203 @@
*****************************************************************************/
<template>
<div
class="w-direction-rose"
:class="compassRoseSizingClasses"
<div ref="compassRoseWrapper"
class="w-direction-rose"
:class="compassRoseSizingClasses"
@click="toggleLockCompass"
>
<div
class="c-direction-rose"
@click="toggleLockCompass"
<svg ref="compassRoseSvg"
class="c-compass-rose-svg"
viewBox="0 0 100 100"
>
<div
class="c-nsew"
:style="compassRoseStyle"
<mask id="mask0"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="100"
height="100"
>
<svg
class="c-nsew__minor-ticks"
viewBox="0 0 100 100"
<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)"
>
<rect
class="c-nsew__tick c-tick-ne"
x="49"
y="0"
width="2"
height="5"
<!-- Background and clipped elements -->
<rect class="c-cr__bg"
width="100"
height="100"
fill="black"
/>
<rect
class="c-nsew__tick c-tick-se"
x="95"
y="49"
width="5"
height="2"
<rect class="c-cr__edge"
width="100"
height="100"
fill="url(#paint0_radial)"
/>
<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"
<rect v-if="hasSunHeading"
class="c-cr__sun"
width="100"
height="100"
fill="url(#paint1_radial)"
:style="sunHeadingStyle"
/>
</svg>
<!-- 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
class="c-nsew__ticks"
viewBox="0 0 100 100"
<!-- 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"
/>
<!-- NSEW and ticks -->
<g class="c-cr__nsew"
:style="compassRoseStyle"
>
<polygon
class="c-nsew__tick c-tick-n"
points="50,0 60,10 40,10"
<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"
/>
<rect
class="c-nsew__tick c-tick-e"
x="95"
y="49"
width="5"
height="2"
<stop offset="1"
stop-color="white"
/>
<rect
class="c-nsew__tick c-tick-w"
x="0"
y="49"
width="5"
height="2"
</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"
/>
<rect
class="c-nsew__tick c-tick-s"
x="49"
y="95"
width="2"
height="5"
<stop offset="1"
stop-color="#FF9900"
stop-opacity="0"
/>
<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>
</radialGradient>
</defs>
</svg>
</div>
</template>
<script>
import { rotate } from './utils';
import { throttle } from 'lodash';
export default {
props: {
sizedImageWidth: {
type: Number,
compassRoseSizingClasses: {
type: String,
required: true
},
heading: {
type: Number,
required: true
required: true,
default() {
return 0;
}
},
sunHeading: {
type: Number,
@@ -178,58 +229,39 @@ export default {
},
cameraPan: {
type: Number,
required: true
required: true,
default() {
return 0;
}
},
lockCompass: {
type: Boolean,
sizedImageDimensions: {
type: Object,
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;
},
northTextTransform() {
return this.cardinalPointsTextTransform.north;
cardinalTextRotateN() {
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
},
eastTextTransform() {
return this.cardinalPointsTextTransform.east;
cardinalTextRotateS() {
return { transform: `translateY(30%) rotate(${ -this.north }deg)` };
},
southTextTransform() {
return this.cardinalPointsTextTransform.south;
cardinalTextRotateE() {
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 }`
};
cardinalTextRotateW() {
return { transform: `translateX(-30%) rotate(${ -this.north }deg)` };
},
hasHeading() {
return this.heading !== undefined;
@@ -238,7 +270,7 @@ export default {
const rotation = rotate(this.north, this.heading);
return {
transform: `translateX(-50%) rotate(${ rotation }deg)`
transform: `rotate(${ rotation }deg)`
};
},
hasSunHeading() {
@@ -262,20 +294,37 @@ export default {
// rotated counter-clockwise from camera pan angle
cameraFOVStyleLeftHalf() {
return {
transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)`
transform: `rotate(${ this.cameraAngleOfView / 2 }deg)`
};
},
// right half of camera field of view
// rotated clockwise from camera pan angle
cameraFOVStyleRightHalf() {
return {
transform: `translateX(-50%) rotate(${ this.cameraAngleOfView / 2 }deg)`
transform: `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.$emit('toggle-lock-compass');
this.lockCompass = !this.lockCompass;
}
}
};

View File

@@ -12,9 +12,8 @@ $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: 50%;
top: 50%;
transform: translate(-50%, -50%);
left: 0;
top: 0;
z-index: 1;
@include userSelectNone;
}
@@ -81,114 +80,55 @@ $elemBg: rgba(black, 0.7);
transform: translateX(-50%);
z-index: 1;
}
}
/***************************** COMPASS DIRECTIONS */
.c-nsew {
/***************************** COMPASS SVG */
.c-compass-rose-svg {
$color: $interfaceKeyColor;
$inset: 5%;
$tickHeightPerc: 15%;
text-shadow: black 0 0 10px;
top: $inset;
right: $inset;
bottom: $inset;
left: $inset;
z-index: 3;
position: absolute;
top: 0; left: 0;
&__tick,
&__label {
fill: $color;
}
&__minor-ticks {
opacity: 0.5;
g, path, rect {
// In an SVG, rotation occurs about the center of the SVG, not the element
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;
.c-cr {
&__bg {
fill: #000;
opacity: 0.8;
}
// 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;
}
&__edge {
opacity: 0.1;
}
&-r {
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
.cam-field-area {
transform-origin: right center;
}
&__sun {
opacity: 0.7;
}
}
}
/***************************** 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()
&__cam-fov-l,
&__cam-fov-r {
// Cam FOV indication
opacity: 0.2;
fill: #fff;
}
&: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);
&__nsew-text,
&__spacecraft-body,
&__ticks-major,
&__ticks-minor {
fill: $color;
}
&__ticks-minor {
opacity: 0.5;
transform: rotate(45deg);
}
&__spacecraft-body {
opacity: 0.3;
}
}
}
@@ -196,32 +136,28 @@ $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 {
.c-nsew__minor-ticks,
.c-tick-w,
.c-tick-s,
.c-tick-e,
.c-label-w,
.c-label-s,
.c-label-e {
.--hide-small {
display: none;
}
.c-label-n {
font-size: 2.5em;
}
}
&.--rose-max {
@@ -230,44 +166,3 @@ $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,28 +55,33 @@
></a>
</span>
</div>
<div class="c-imagery__main-image__bg"
<div ref="imageBG"
class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }"
>
<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"
<div class="image-wrapper"
:style="{
'width': `${sizedImageDimensions.width}px`,
'height': `${sizedImageDimensions.height}px`
}"
>
<Compass
v-if="shouldDisplayCompass"
:container-width="imageContainerWidth"
:container-height="imageContainerHeight"
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
:image="focusedImage"
:lock-compass="lockCompass"
@toggle-lock-compass="toggleLockCompass"
/>
<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>
</div>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev"
@@ -124,27 +129,40 @@
</div>
</div>
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-wrapper"
:class="{'is-paused': isPaused}"
@scroll="handleScroll"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
>
<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
ref="thumbsWrapper"
class="c-imagery__thumbs-scroll-area"
@scroll="handleScroll"
>
<a href=""
:download="image.imageDownloadName"
@click.prevent
<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)"
>
<img class="c-thumb__image"
:src="image.url"
<a href=""
:download="image.imageDownloadName"
@click.prevent
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
<img class="c-thumb__image"
:src="image.url"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</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,6 +189,8 @@ const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
const ARROW_RIGHT = 39;
const ARROW_LEFT = 37;
const SCROLL_LATENCY = 250;
export default {
components: {
Compass
@@ -204,10 +224,23 @@ export default {
focusedImageNaturalAspectRatio: undefined,
imageContainerWidth: undefined,
imageContainerHeight: undefined,
lockCompass: true
lockCompass: true,
resizingWindow: false
};
},
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);
},
@@ -331,6 +364,20 @@ 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: {
@@ -379,10 +426,14 @@ export default {
_.debounce(this.resizeImageContainer, 400);
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.focusedImage);
},
updated() {
this.scrollToRight();
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);
},
beforeDestroy() {
if (this.unsubscribe) {
@@ -394,6 +445,10 @@ export default {
this.imageContainerResizeObserver.disconnect();
}
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
}
@@ -561,17 +616,15 @@ export default {
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper) {
if (!thumbsWrapper || this.resizingWindow) {
return;
}
const { scrollLeft, scrollWidth, clientWidth, scrollTop, scrollHeight, clientHeight } = thumbsWrapper;
const disableScroll = (scrollWidth - scrollLeft) > 2 * clientWidth
|| (scrollHeight - scrollTop) > 2 * clientHeight;
const { scrollLeft, scrollWidth, clientWidth } = thumbsWrapper;
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
this.autoScroll = !disableScroll;
},
paused(state, type) {
this.isPaused = state;
if (type === 'button') {
@@ -584,6 +637,7 @@ export default {
}
this.autoScroll = true;
this.scrollToRight();
},
scrollToFocused() {
const thumbsWrapper = this.$refs.thumbsWrapper;
@@ -600,8 +654,8 @@ export default {
});
}
},
scrollToRight() {
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
scrollToRight(type) {
if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
return;
}
@@ -610,7 +664,9 @@ export default {
return;
}
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
this.$nextTick(() => {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
},
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
@@ -678,9 +734,9 @@ export default {
image.imageDownloadName = this.getImageDownloadName(datum);
this.imageHistory.push(image);
if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1);
this.scrollToRight();
}
},
getFormatter(key) {
@@ -808,16 +864,31 @@ export default {
}, { once: true });
},
resizeImageContainer() {
if (this.$refs.focusedImage.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.focusedImage.clientWidth;
if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.imageBG.clientWidth;
}
if (this.$refs.focusedImage.clientHeight !== this.imageContainerHeight) {
this.imageContainerHeight = this.$refs.focusedImage.clientHeight;
if (this.$refs.imageBG.clientHeight !== this.imageContainerHeight) {
this.imageContainerHeight = this.$refs.imageBG.clientHeight;
}
},
toggleLockCompass() {
this.lockCompass = !this.lockCompass;
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;
});
}
}
};

View File

@@ -22,6 +22,9 @@
&__bg {
background-color: $colorPlotBg;
border: 1px solid transparent;
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 auto;
height: 0;
@@ -33,7 +36,6 @@
&__image {
height: 100%;
width: 100%;
object-fit: contain;
}
}
@@ -93,24 +95,43 @@
}
&__thumbs-wrapper {
flex: 0 0 auto;
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;
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 */
@@ -142,7 +163,7 @@
.l-layout,
.c-fl {
.c-imagery__thumbs-wrapper {
.c-imagery__thumbs-scroll-area {
// When Imagery is in a layout, hide the thumbs area
display: none;
}
@@ -173,6 +194,10 @@
margin-right: $interiorMarginSm;
}
}
.s-status-taking-snapshot & {
display: none;
}
}
&__lc {
@@ -254,6 +279,10 @@
content: $glyph-icon-play;
}
}
.s-status-taking-snapshot & {
display: none;
}
}
.c-imagery__prev-next-buttons {
@@ -268,6 +297,10 @@
.c-nav {
pointer-events: all;
}
.s-status-taking-snapshot & {
display: none;
}
}
.c-nav {

View File

@@ -92,6 +92,7 @@ describe("The Imagery View Layout", () => {
let resolveFunction;
let openmct;
let appHolder;
let parent;
let child;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
@@ -195,7 +196,7 @@ describe("The Imagery View Layout", () => {
// this setups up the app
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
@@ -209,6 +210,8 @@ describe("The Imagery View Layout", () => {
child = document.createElement('div');
parent.appendChild(child);
// document.querySelector('body').append(parent);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
disconnect() {}
@@ -362,5 +365,21 @@ 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

@@ -1,3 +1,25 @@
<!--
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.
-->
<template>
<div ref="plan"
class="c-plan c-timeline-holder"
@@ -28,7 +50,6 @@ import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import { getValidatedPlan } from "./util";
import Vue from "vue";
//TODO: UI direction needed for the following property values
const PADDING = 1;
const OUTER_TEXT_PADDING = 12;
const INNER_TEXT_PADDING = 17;
@@ -281,7 +302,9 @@ export default {
exceeds: {
start: this.xScale(this.viewBounds.start) > this.xScale(activity.start),
end: this.xScale(this.viewBounds.end) < this.xScale(activity.end)
}
},
start: activity.start,
end: activity.end
},
textLines: textLines,
textStart: textStart,
@@ -339,6 +362,9 @@ export default {
components: {
SwimLane
},
provide: {
openmct: this.openmct
},
data() {
return {
heading,
@@ -376,7 +402,6 @@ export default {
activityRows.forEach((row) => {
const items = activitiesByRow[row];
items.forEach(item => {
//TODO: Don't draw the left-border of the rectangle if the activity started before viewBounds.start
this.plotActivity(item, parseInt(row, 10), groupSVG);
});
});
@@ -399,6 +424,9 @@ export default {
element.setAttributeNS(null, key, attributes[key]);
});
},
getNSAttributesForElement(element, attribute) {
return element.getAttributeNS(null, attribute);
},
// Experimental for now - unused
addForeignElement(svgElement, label, x, y) {
let foreign = document.createElementNS('http://www.w3.org/2000/svg', "foreignObject");
@@ -443,6 +471,10 @@ export default {
fill: activity.color
});
rectElement.addEventListener('click', (event) => {
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
});
svgElement.appendChild(rectElement);
item.textLines.forEach((line, index) => {
@@ -456,6 +488,9 @@ export default {
const textNode = document.createTextNode(line);
textElement.appendChild(textNode);
textElement.addEventListener('click', (event) => {
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
});
svgElement.appendChild(textElement);
});
// this.addForeignElement(svgElement, activity.name, item.textStart, item.textY - LINE_HEIGHT);
@@ -482,6 +517,22 @@ export default {
const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000;
return cBrightness > cThreshold ? "#000000" : "#ffffff";
},
setSelectionForActivity(element, activity, multiSelect) {
this.openmct.selection.select([{
element: element,
context: {
type: 'activity',
activity: activity
}
}, {
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject,
supportsMultiSelect: true
}
}], multiSelect);
event.stopPropagation();
}
}
};

View File

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

View File

@@ -0,0 +1,206 @@
<!--
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>
<div class="c-inspector__properties c-inspect-properties">
<plan-activity-view v-for="activity in activities"
:key="activity.id"
:activity="activity"
:heading="heading"
/>
</div>
</template>
<script>
import PlanActivityView from "./PlanActivityView.vue";
import { getPreciseDuration } from "utils/duration";
import uuid from 'uuid';
const propertyLabels = {
'start': 'Start DateTime',
'end': 'End DateTime',
'duration': 'Duration',
'earliestStart': 'Earliest Start',
'latestEnd': 'Latest End',
'gap': 'Gap',
'overlap': 'Overlap',
'totalTime': 'Total Time'
};
export default {
components: {
PlanActivityView
},
inject: ['openmct', 'selection'],
data() {
return {
name: '',
activities: [],
heading: ''
};
},
mounted() {
this.setFormatters();
this.getPlanData(this.selection);
this.getActivities();
this.openmct.selection.on('change', this.updateSelection);
this.openmct.time.on('timeSystem', this.setFormatters);
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
this.openmct.time.off('timeSystem', this.setFormatters);
},
methods: {
setFormatters() {
let timeSystem = this.openmct.time.timeSystem();
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
format: timeSystem.timeFormat
}).formatter;
},
updateSelection(newSelection) {
this.getPlanData(newSelection);
this.getActivities();
},
getPlanData(selection) {
this.selectedActivities = [];
selection.forEach((selectionItem) => {
if (selectionItem[0].context.type === 'activity') {
const activity = selectionItem[0].context.activity;
if (activity) {
this.selectedActivities.push(activity);
}
}
});
},
getActivities() {
if (this.selectedActivities.length <= 1) {
this.heading = 'Time';
this.setSingleActivityProperties();
} else {
this.heading = 'Convex Hull';
this.setMultipleActivityProperties();
}
},
setSingleActivityProperties() {
this.activities.splice(0);
this.selectedActivities.forEach((selectedActivity, index) => {
const activity = {
id: uuid(),
start: {
label: propertyLabels.start,
value: this.formatTime(selectedActivity.start)
},
end: {
label: propertyLabels.end,
value: this.formatTime(selectedActivity.end)
},
duration: {
label: propertyLabels.duration,
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
}
};
this.$set(this.activities, index, activity);
});
},
sortFn(a, b) {
const numA = parseInt(a.start, 10);
const numB = parseInt(b.start, 10);
if (numA > numB) {
return 1;
}
if (numA < numB) {
return -1;
}
return 0;
},
setMultipleActivityProperties() {
this.activities.splice(0);
let earliestStart;
let latestEnd;
let gap;
let overlap;
//Sort by start time
let selectedActivities = this.selectedActivities.sort(this.sortFn);
selectedActivities.forEach((selectedActivity, index) => {
if (selectedActivities.length === 2 && index > 0) {
const previous = selectedActivities[index - 1];
//they're on different rows so there must be overlap
if (previous.end > selectedActivity.start) {
overlap = previous.end - selectedActivity.start;
} else if (previous.end < selectedActivity.start) {
gap = selectedActivity.start - previous.end;
}
}
if (index > 0) {
earliestStart = Math.min(earliestStart, selectedActivity.start);
latestEnd = Math.max(latestEnd, selectedActivity.end);
} else {
earliestStart = selectedActivity.start;
latestEnd = selectedActivity.end;
}
});
let totalTime = latestEnd - earliestStart;
const activity = {
id: uuid(),
'earliestStart': {
label: propertyLabels.earliestStart,
value: this.formatTime(earliestStart)
},
'latestEnd': {
label: propertyLabels.latestEnd,
value: this.formatTime(latestEnd)
}
};
if (gap) {
activity.gap = {
label: propertyLabels.gap,
value: this.formatDuration(gap)
};
} else if (overlap) {
activity.overlap = {
label: propertyLabels.overlap,
value: this.formatDuration(overlap)
};
}
activity.totalTime = {
label: propertyLabels.totalTime,
value: this.formatDuration(totalTime)
};
this.$set(this.activities, 0, activity);
},
formatDuration(duration) {
return getPreciseDuration(duration);
},
formatTime(time) {
return this.timeFormatter.format(time);
}
}
};
</script>

View File

@@ -0,0 +1,84 @@
<!--
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.
-->
<template>
<div v-if="timeProperties.length"
class="u-contents"
>
<div class="c-inspect-properties__header">
{{ heading }}
</div>
<ul v-for="timeProperty in timeProperties"
:key="timeProperty.id"
class="c-inspect-properties__section"
>
<activity-property :label="timeProperty.label"
:value="timeProperty.value"
/>
</ul>
</div>
</template>
<script>
import ActivityProperty from './ActivityProperty.vue';
import uuid from 'uuid';
export default {
components: {
ActivityProperty
},
props: {
activity: {
type: Object,
required: true
},
heading: {
type: String,
required: true
}
},
data() {
return {
timeProperties: []
};
},
mounted() {
this.setProperties();
},
methods: {
setProperties() {
Object.keys(this.activity).forEach((key) => {
if (this.activity[key].label) {
const label = this.activity[key].label;
const value = String(this.activity[key].value);
this.$set(this.timeProperties, this.timeProperties.length, {
id: uuid(),
label,
value
});
}
});
}
}
};
</script>

View File

@@ -0,0 +1,69 @@
/*****************************************************************************
* 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 PlanActivitiesView from "./PlanActivitiesView.vue";
import Vue from 'vue';
export default function PlanInspectorViewProvider(openmct) {
return {
key: 'plan-inspector',
name: 'Plan Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let context = selection[0][0].context;
return context
&& context.type === 'activity';
},
view: function (selection) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
PlanActivitiesView: PlanActivitiesView
},
provide: {
openmct,
selection: selection
},
template: '<plan-activities-view></plan-activities-view>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -1,3 +1,25 @@
/*****************************************************************************
* 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.
*****************************************************************************/
.c-plan {
svg {
text-rendering: geometricPrecision;

View File

@@ -21,6 +21,7 @@
*****************************************************************************/
import PlanViewProvider from './PlanViewProvider';
import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider";
export default function () {
return function install(openmct) {
@@ -44,6 +45,7 @@ export default function () {
}
});
openmct.objectViews.addProvider(new PlanViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct));
};
}

View File

@@ -1,3 +1,25 @@
/*****************************************************************************
* 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.
*****************************************************************************/
export function getValidatedPlan(domainObject) {
let body = domainObject.selectFile.body;
let json = {};

View File

@@ -74,7 +74,9 @@
</div>
<div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover">
<div class="c-button-set c-button-set--strip-h">
<div v-if="!options.compact"
class="c-button-set c-button-set--strip-h js-zoom"
>
<button class="c-button icon-minus"
title="Zoom out"
@click="zoom('out', 0.2)"
@@ -86,8 +88,8 @@
>
</button>
</div>
<div v-if="plotHistory.length"
class="c-button-set c-button-set--strip-h"
<div v-if="plotHistory.length && !options.compact"
class="c-button-set c-button-set--strip-h js-pan"
>
<button class="c-button icon-arrow-left"
title="Restore previous pan/zoom"
@@ -100,8 +102,8 @@
>
</button>
</div>
<div v-if="isRealTime"
class="c-button-set c-button-set--strip-h"
<div v-if="isRealTime && !options.compact"
class="c-button-set c-button-set--strip-h js-pause"
>
<button v-if="!isFrozen"
class="c-button icon-pause"
@@ -493,10 +495,12 @@ export default {
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
if (!this.options.compact) {
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
}
},
initialize() {
@@ -514,12 +518,7 @@ export default {
this.chartElementBounds = undefined;
this.tickUpdate = false;
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
this.initCanvas();
this.config.yAxisLabel = this.config.yAxis.get('label');

View File

@@ -403,6 +403,25 @@ describe("the plugin", function () {
});
});
describe('controls in time strip view', () => {
it('zoom controls are hidden', () => {
let pauseEl = element.querySelectorAll(".c-button-set .js-zoom");
expect(pauseEl.length).toBe(0);
});
it('pan controls are hidden', () => {
let pauseEl = element.querySelectorAll(".c-button-set .js-pan");
expect(pauseEl.length).toBe(0);
});
it('pause/play controls are hidden', () => {
let pauseEl = element.querySelectorAll(".c-button-set .js-pause");
expect(pauseEl.length).toBe(0);
});
});
});
describe("The stacked plot view", () => {

View File

@@ -65,7 +65,8 @@ define([
'./interceptors/plugin',
'./performanceIndicator/plugin',
'./CouchDBSearchFolder/plugin',
'./timeline/plugin'
'./timeline/plugin',
'/node_modules/openmct-mmgis/dist/openmct-mmgis'
], function (
_,
UTCTimeSystem,
@@ -111,7 +112,8 @@ define([
ObjectInterceptors,
PerformanceIndicator,
CouchDBSearchFolder,
Timeline
Timeline,
Mmgis
) {
const bundleMap = {
LocalStorage: 'platform/persistence/local',
@@ -212,6 +214,7 @@ define([
plugins.PerformanceIndicator = PerformanceIndicator.default;
plugins.CouchDBSearchFolder = CouchDBSearchFolder.default;
plugins.Timeline = Timeline.default;
plugins.Mmgis = Mmgis.default;
return plugins;
});

View File

@@ -39,9 +39,8 @@ const DEFAULT_DURATION_FORMATTER = 'duration';
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
const DEFAULT_RECORDS = 10;
const ONE_MINUTE = 60 * 1000;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
import { getDuration } from "utils/duration";
export default {
inject: ['openmct', 'configuration'],
@@ -143,7 +142,7 @@ export default {
let description = `${startTime} - ${this.formatTime(timespan.end)}`;
if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) {
name = `${startTime} ${this.getDuration(timespan.end - timespan.start)}`;
name = `${startTime} ${getDuration(timespan.end - timespan.start)}`;
} else {
name = description;
}
@@ -176,41 +175,6 @@ export default {
};
});
},
getDuration(numericDuration) {
let result;
let age;
if (numericDuration > ONE_DAY - 1) {
age = this.normalizeAge((numericDuration / ONE_DAY).toFixed(2));
result = `+ ${age} day`;
if (age !== 1) {
result += 's';
}
} else if (numericDuration > ONE_HOUR - 1) {
age = this.normalizeAge((numericDuration / ONE_HOUR).toFixed(2));
result = `+ ${age} hour`;
if (age !== 1) {
result += 's';
}
} else {
age = this.normalizeAge((numericDuration / ONE_MINUTE).toFixed(2));
result = `+ ${age} min`;
if (age !== 1) {
result += 's';
}
}
return result;
},
normalizeAge(num) {
const hundredtized = num * 100;
const isWhole = hundredtized % 100 === 0;
return isWhole ? hundredtized / 100 : num;
},
getHistoryFromLocalStorage() {
const localStorageHistory = localStorage.getItem(this.storageKey);
const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;

View File

@@ -3,6 +3,8 @@
class="pr-tc-input-menu"
@keydown.enter.prevent
@keyup.enter.prevent="submit"
@keydown.esc.prevent
@keyup.esc.prevent="hide"
@click.stop
>
<div class="pr-time-label__hrs">Hrs</div>

View File

@@ -26,6 +26,7 @@
background-repeat: no-repeat;
background-size: cover;
background-image: url('../ui/layout/assets/images/bg-splash.jpg');
margin-top: 30px; // Don't overlap with close "X" button
&:before,
&:after {
@@ -95,10 +96,6 @@
&--licenses {
padding: 0 10%;
.c-license {
&__text {
color: pushBack($overlayColorFg, 20%);
}
+ .c-license {
border-top: 1px solid $colorInteriorBorder;
margin-top: 2em;
@@ -111,7 +108,7 @@
}
em {
color: pushBack($overlayColorFg, 20%);
color: pushBack($colorBodyFg, 20%);
}
h1, h2, h3 {

View File

@@ -290,12 +290,7 @@ $colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);
// Overlay
$colorOvrBlocker: rgba(black, 0.7);
$overlayColorBg: $colorMenuBg;
$overlayColorFg: $colorMenuFg;
$colorOvrBtnBg: pullForward($overlayColorBg, 20%);
$colorOvrBtnFg: #fff;
$overlayCr: $interiorMarginLg;
$overlayBrightnessAdjust: brightness(1.3); // Applied in a filter: property
$overlayCr: $interiorMargin;
// Indicator colors
$colorIndicatorAvailable: $colorKey;

View File

@@ -294,12 +294,7 @@ $colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);
// Overlay
$colorOvrBlocker: rgba(black, 0.7);
$overlayColorBg: $colorMenuBg;
$overlayColorFg: $colorMenuFg;
$colorOvrBtnBg: pullForward($overlayColorBg, 20%);
$colorOvrBtnFg: #fff;
$overlayCr: $interiorMarginLg;
$overlayBrightnessAdjust: brightness(1.3); // Applied in a filter: property
// Indicator colors
$colorIndicatorAvailable: $colorKey;

View File

@@ -290,12 +290,7 @@ $colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);
// Overlay
$colorOvrBlocker: rgba(black, 0.7);
$overlayColorBg: $colorMenuBg;
$overlayColorFg: $colorMenuFg;
$colorOvrBtnBg: pullForward($overlayColorBg, 40%);
$colorOvrBtnFg: #fff;
$overlayCr: $interiorMarginLg;
$overlayBrightnessAdjust: brightness(1); // Applied in a filter: property
// Indicator colors
$colorIndicatorAvailable: $colorKey;

View File

@@ -40,7 +40,8 @@ $inputTextP: $inputTextPTopBtm $inputTextPLeftRight;
$menuLineH: 1.5rem;
$treeItemIndent: 16px;
$treeTypeIconW: 18px;
$overlayOuterMarginFullscreen: 0%;
$overlayOuterMarginFullscreen: 0;
$overlayOuterMarginLarge: 10px;
$overlayOuterMarginDialog: 20%;
$overlayInnerMargin: 25px;
$mainViewPad: 0px;

View File

@@ -0,0 +1,43 @@
/*****************************************************************************
* 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>
<li class="c-inspect-properties__row">
<div class="c-inspect-properties__label">
{{ detail.name }}
</div>
<div class="c-inspect-properties__value">
{{ detail.value }}
</div>
</li>
</template>
<script>
export default {
props: {
detail: {
type: Object,
required: true
}
}
};
</script>

View File

@@ -0,0 +1,50 @@
/*****************************************************************************
* 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>
<li class="c-inspect-properties__row">
<div class="c-inspect-properties__label">
{{ detail.name }}
</div>
<div class="c-inspect-properties__value">
{{ formattedTime }}
</div>
</li>
</template>
<script>
import Moment from 'moment';
export default {
props: {
detail: {
type: Object,
required: true
}
},
computed: {
formattedTime() {
return Moment.utc(this.detail.value).format('YYYY-MM-DD[\n]HH:mm:ss') + ' UTC';
}
}
};
</script>

View File

@@ -1,3 +1,25 @@
/*****************************************************************************
* 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>
<li
draggable="true"

View File

@@ -1,3 +1,25 @@
/*****************************************************************************
* 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>
<div class="c-elements-pool">
<Search

View File

@@ -1,3 +1,25 @@
/*****************************************************************************
* 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>
<div class="c-inspector">
<object-name />
@@ -19,7 +41,9 @@
type="vertical"
>
<pane class="c-inspector__properties">
<properties />
<Properties
v-if="!activity"
/>
<location />
<inspector-views />
</pane>
@@ -98,7 +122,8 @@ export default {
key: '__styles',
name: 'Styles'
}],
currentTabbedView: {}
currentTabbedView: {},
activity: undefined
};
},
mounted() {
@@ -111,9 +136,12 @@ export default {
methods: {
updateInspectorViews(selection) {
this.refreshComposition(selection);
if (this.openmct.types.get('conditionSet')) {
this.refreshTabs(selection);
}
this.setActivity(selection);
},
refreshComposition(selection) {
if (selection.length > 0 && selection[0].length > 0) {
@@ -150,6 +178,12 @@ export default {
},
isCurrent(view) {
return _.isEqual(this.currentTabbedView, view);
},
setActivity(selection) {
this.activity = selection
&& selection.length
&& selection[0].length
&& selection[0][0].activity;
}
}
};

View File

@@ -0,0 +1,218 @@
/*****************************************************************************
* 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 {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import {
mockLocalStorage
} from 'utils/testing/mockLocalStorage';
import {
mockTelemetryTableSelection,
mockMultiSelectionSameStyles,
mockMultiSelectionMixedStyles,
mockMultiSelectionNonSpecificStyles,
mockStyle
} from './InspectorStylesSpecMocks';
import Vue from 'vue';
import StylesView from '@/plugins/condition/components/inspector/StylesView.vue';
import SavedStylesView from '@/ui/inspector/styles/SavedStylesView.vue';
import stylesManager from '@/ui/inspector/styles/StylesManager';
describe("the inspector", () => {
let openmct;
let selection;
let stylesViewComponent;
let savedStylesViewComponent;
mockLocalStorage();
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it("should allow a style to be saved", () => {
selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(0);
stylesViewComponent.$children[0].saveStyle(mockStyle);
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(1);
});
it("should display all saved styles", () => {
selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
expect(savedStylesViewComponent.$children[0].$children.length).toBe(0);
stylesViewComponent.$children[0].saveStyle(mockStyle);
stylesViewComponent.$nextTick().then(() => {
expect(savedStylesViewComponent.$children[0].$children.length).toBe(1);
});
});
it("should allow a saved style to be applied", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$children[0].saveStyle(mockStyle);
stylesViewComponent.$nextTick().then(() => {
const styleSelectorComponent = savedStylesViewComponent.$children[0].$children[0];
styleSelectorComponent.selectStyle();
savedStylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const styles = styleEditorComponent.$children.filter(component => component.options.value === mockStyle.color);
expect(styles.length).toBe(3);
});
});
});
it("should allow a saved style to be deleted", () => {
selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$children[0].saveStyle(mockStyle);
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(1);
savedStylesViewComponent.$children[0].deleteStyle(0);
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(0);
});
it("should prevent a style from being saved when the number of saved styles is at the limit", () => {
spyOn(SavedStylesView.methods, 'showLimitReachedDialog').and.callThrough();
selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
for (let i = 1; i <= 20; i++) {
stylesViewComponent.$children[0].saveStyle(mockStyle);
}
expect(SavedStylesView.methods.showLimitReachedDialog).not.toHaveBeenCalled();
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(20);
stylesViewComponent.$children[0].saveStyle(mockStyle);
expect(SavedStylesView.methods.showLimitReachedDialog).toHaveBeenCalled();
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(20);
});
it("should allow styles from multi-selections to be saved", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockMultiSelectionSameStyles;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;
const saveStyleButton = styleEditorComponent.$children[saveStyleButtonIndex];
expect(saveStyleButton.$listeners.click).not.toBe(undefined);
saveStyleButton.$listeners.click();
expect(savedStylesViewComponent.$children[0].savedStyles.length).toBe(1);
});
});
it("should prevent mixed styles from being saved", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockMultiSelectionMixedStyles;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;
const saveStyleButton = styleEditorComponent.$children[saveStyleButtonIndex];
expect(saveStyleButton.$listeners.click).toBe(undefined);
});
});
it("should prevent non-specific styles from being saved", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockMultiSelectionNonSpecificStyles;
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;
const saveStyleButton = styleEditorComponent.$children[saveStyleButtonIndex];
expect(saveStyleButton.$listeners.click).toBe(undefined);
});
});
function createViewComponent(component) {
const element = document.createElement('div');
const child = document.createElement('div');
element.appendChild(child);
const config = {
provide: {
openmct,
selection,
stylesManager
},
el: element,
components: {},
template: `<${component.name} />`
};
config.components[component.name] = component;
return new Vue(config).$mount();
}
});

View File

@@ -0,0 +1,226 @@
/*****************************************************************************
* 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.
*****************************************************************************/
export const mockTelemetryTableSelection = [
[{
context: {
item: {
configuration: {},
type: 'table',
identifier: {
key: 'mock-telemetry-table-1',
namespace: ''
}
}
}
}]
];
export const mockStyle = {
backgroundColor: '#ff0000',
border: '#ff0000',
color: '#ff0000'
};
const mockDisplayLayoutPath = {
context: {
item: {
identifier: {
key: "6af3200d-928b-4ff0-8ed0-b94a0e6752d1",
namespace: ""
},
type: "layout",
configuration: {
items: [
{
id: "dd3202e5-40d0-4112-8951-00f0f1ed6a29",
type: "text-view",
fontSize: "default",
font: "default"
},
{
id: "b522d636-90b2-4f5f-9588-2a0345c30f87",
type: "text-view",
fontSize: "default",
font: "default"
},
{
id: "537b7596-b442-44fe-b464-07f56bdc67c8",
type: "text-view",
fontSize: "default",
font: "default"
},
{
id: "3f17162f-a822-4e39-8332-6aa39b79d022",
type: "text-view",
fontSize: "default",
font: "default"
},
{
id: "c1c5acd8-a14b-450c-8c94-ce0075dd9912",
type: "text-view",
fontSize: "8",
font: "monospace-bold"
}
],
objectStyles: {
"dd3202e5-40d0-4112-8951-00f0f1ed6a29": {
staticStyle: {
style: {
backgroundColor: "#0000ff",
border: "1px solid #0000ff"
}
}
},
"b522d636-90b2-4f5f-9588-2a0345c30f87": {
staticStyle: {
style: {
backgroundColor: "#ff0000",
border: "1px solid #ff0000"
}
}
},
"537b7596-b442-44fe-b464-07f56bdc67c8": {
staticStyle: {
style: {
backgroundColor: "#ff0000",
border: "1px solid #ff0000"
}
}
},
"3f17162f-a822-4e39-8332-6aa39b79d022": {
staticStyle: {
style: {
backgroundColor: "#0000ff",
border: "1px solid #0000ff",
color: "#0000ff"
}
}
},
"c1c5acd8-a14b-450c-8c94-ce0075dd9912": {
staticStyle: {
style: {
backgroundColor: "#0000ff",
border: "1px solid #0000ff",
color: "#0000ff"
}
}
}
}
}
},
supportsMultiSelect: true
}
};
const mockTextBox1Path = {
context: {
index: 0,
layoutItem: {
id: "dd3202e5-40d0-4112-8951-00f0f1ed6a29",
type: "text-view",
fontSize: "default",
font: "default"
}
}
};
const mockTextBox2Path = {
context: {
index: 1,
layoutItem: {
id: "b522d636-90b2-4f5f-9588-2a0345c30f87",
type: "text-view",
fontSize: "default",
font: "default"
}
}
};
const mockTextBox3Path = {
context: {
index: 2,
layoutItem: {
id: "537b7596-b442-44fe-b464-07f56bdc67c8",
type: "text-view",
fontSize: "default",
font: "default"
}
}
};
const mockTextBox4Path = {
context: {
index: 3,
layoutItem: {
id: "3f17162f-a822-4e39-8332-6aa39b79d022",
type: "text-view",
fontSize: "default",
font: "default"
}
}
};
const mockTextBox5Path = {
context: {
index: 4,
layoutItem: {
id: "c1c5acd8-a14b-450c-8c94-ce0075dd9912",
type: "text-view",
fontSize: "8",
font: "default-bold"
}
}
};
export const mockMultiSelectionSameStyles = [
[
mockTextBox2Path,
mockDisplayLayoutPath
],
[
mockTextBox3Path,
mockDisplayLayoutPath
]
];
export const mockMultiSelectionMixedStyles = [
[
mockTextBox1Path,
mockDisplayLayoutPath
],
[
mockTextBox2Path,
mockDisplayLayoutPath
]
];
export const mockMultiSelectionNonSpecificStyles = [
[
mockTextBox4Path,
mockDisplayLayoutPath
],
[
mockTextBox5Path,
mockDisplayLayoutPath
]
];

View File

@@ -1,10 +1,29 @@
/*****************************************************************************
* 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>
<div></div>
</template>
<style>
</style>
<script>
export default {
inject: ['openmct'],

View File

@@ -1,3 +1,25 @@
/*****************************************************************************
* 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>
<div class="c-inspect-properties c-inspect-properties--location">
<div

View File

@@ -1,3 +1,25 @@
/*****************************************************************************
* 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>
<div class="c-inspector__header">
<div v-if="!multiSelect"
@@ -20,7 +42,12 @@
<span class="c-object-label__type-icon"
:class="typeCssClass"
></span>
<span class="c-object-label__name">Layout Object</span>
<span v-if="!activity"
class="c-object-label__name"
>Layout Object</span>
<span v-else
class="c-object-label__name"
>{{ activity.name }}</span>
</div>
</div>
<div v-if="multiSelect"
@@ -37,6 +64,7 @@ export default {
data() {
return {
domainObject: {},
activity: undefined,
keyString: undefined,
multiSelect: false,
itemsSelected: 0,
@@ -51,6 +79,10 @@ export default {
return this.openmct.types.get(this.item.type);
},
typeCssClass() {
if (this.activity) {
return 'icon-activity';
}
if (this.type.definition.cssClass === undefined) {
return 'icon-object';
}
@@ -97,7 +129,7 @@ export default {
} else {
this.multiSelect = false;
this.domainObject = selection[0][0].context.item;
this.activity = selection[0][0].context.activity;
if (this.domainObject) {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.status = this.openmct.status.get(this.keyString);

View File

@@ -1,102 +1,149 @@
/*****************************************************************************
* 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>
<div class="c-inspector__properties c-inspect-properties">
<div class="c-inspect-properties__header">
Details
</div>
<ul
v-if="!multiSelect && !singleSelectNonObject"
v-if="hasDetails"
class="c-inspect-properties__section"
>
<li class="c-inspect-properties__row">
<div class="c-inspect-properties__label">
Title
</div>
<div class="c-inspect-properties__value">
{{ item.name }}
</div>
</li>
<li class="c-inspect-properties__row">
<div class="c-inspect-properties__label">
Type
</div>
<div class="c-inspect-properties__value">
{{ typeName }}
</div>
</li>
<li
v-if="item.created"
class="c-inspect-properties__row"
>
<div class="c-inspect-properties__label">
Created
</div>
<div class="c-inspect-properties__value">
{{ formatTime(item.created) }}
</div>
</li>
<li
v-if="item.modified"
class="c-inspect-properties__row"
>
<div class="c-inspect-properties__label">
Modified
</div>
<div class="c-inspect-properties__value">
{{ formatTime(item.modified) }}
</div>
</li>
<li
v-for="prop in typeProperties"
:key="prop.name"
class="c-inspect-properties__row"
>
<div class="c-inspect-properties__label">
{{ prop.name }}
</div>
<div class="c-inspect-properties__value">
{{ prop.value }}
</div>
</li>
<Component
:is="getComponent(detail)"
v-for="detail in details"
:key="detail.name"
:detail="detail"
/>
</ul>
<div
v-if="multiSelect"
v-else
class="c-inspect-properties__row--span-all"
>
No properties to display for multiple items
</div>
<div
v-if="singleSelectNonObject"
class="c-inspect-properties__row--span-all"
>
No properties to display for this item
{{ noDetailsMessage }}
</div>
</div>
</template>
<script>
import Moment from "moment";
import DetailText from './DetailText.vue';
import DetailTime from './DetailTime.vue';
export default {
components: {
DetailText,
DetailTime
},
inject: ['openmct'],
data() {
return {
domainObject: {},
multiSelect: false
selection: undefined
};
},
computed: {
item() {
return this.domainObject || {};
details() {
return this.customDetails ? this.customDetails : this.domainObjectDetails;
},
type() {
return this.openmct.types.get(this.item.type);
},
typeName() {
if (!this.type) {
return `Unknown: ${this.item.type}`;
customDetails() {
if (this.context === undefined) {
return;
}
return this.type.definition.name;
return this.context.details;
},
domainObject() {
if (this.context === undefined) {
return;
}
return this.context.item;
},
type() {
if (this.domainObject === undefined) {
return;
}
return this.openmct.types.get(this.domainObject.type);
},
domainObjectDetails() {
if (this.domainObject === undefined) {
return;
}
const title = this.domainObject.name;
const typeName = this.type ? this.type.definition.name : `Unknown: ${this.domainObject.type}`;
const timestampLabel = this.domainObject.modified ? 'Modified' : 'Created';
const timestamp = this.domainObject.modified ? this.domainObject.modified : this.domainObject.created;
const details = [
{
name: 'Title',
value: title
},
{
name: 'Type',
value: typeName
}
];
if (timestamp !== undefined) {
details.push(
{
name: timestampLabel,
value: timestamp,
component: 'time'
}
);
}
return [...details, ...this.typeProperties];
},
context() {
if (
!this.selection
|| !this.selection.length
|| !this.selection[0].length
) {
return;
}
return this.selection[0][0].context;
},
hasDetails() {
return Boolean(
this.details
&& this.details.length
&& !this.multiSelection
);
},
multiSelection() {
return this.selection && this.selection.length > 1;
},
noDetailsMessage() {
return this.multiSelection
? 'No properties to display for multiple items'
: 'No properties to display for this item';
},
typeProperties() {
if (!this.type) {
@@ -126,12 +173,9 @@ export default {
name: field.name,
value: field.path.reduce((object, key) => {
return object[key];
}, this.item)
}, this.domainObject)
};
});
},
singleSelectNonObject() {
return !this.item.identifier && !this.multiSelect;
}
},
mounted() {
@@ -142,25 +186,13 @@ export default {
this.openmct.selection.off('change', this.updateSelection);
},
methods: {
updateSelection(selection) {
if (selection.length === 0 || selection[0].length === 0) {
this.domainObject = {};
getComponent(detail) {
const component = detail.component ? detail.component : 'text';
return;
}
if (selection.length > 1) {
this.multiSelect = true;
this.domainObject = {};
return;
} else {
this.multiSelect = false;
this.domainObject = selection[0][0].context.item;
}
return `detail-${component}`;
},
formatTime(unixTime) {
return Moment.utc(unixTime).format('YYYY-MM-DD[\n]HH:mm:ss') + ' UTC';
updateSelection(selection) {
this.selection = selection;
}
}
};

View File

@@ -10,7 +10,6 @@ define([
let unobserve = undefined;
let currentObjectPath;
let isRoutingInProgress = false;
let mutable;
openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot);
openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => {
@@ -37,24 +36,10 @@ define([
}
function viewObject(object, viewProvider) {
if (mutable) {
openmct.objects.destroyMutable(mutable);
mutable = undefined;
}
if (openmct.objects.supportsMutation(object.identifier)) {
mutable = openmct.objects._toMutable(object);
}
currentObjectPath = openmct.router.path;
if (mutable !== undefined) {
openmct.layout.$refs.browseObject.show(mutable, viewProvider.key, true, currentObjectPath);
openmct.layout.$refs.browseBar.domainObject = mutable;
} else {
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath);
openmct.layout.$refs.browseBar.domainObject = object;
}
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath);
openmct.layout.$refs.browseBar.domainObject = object;
openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
}

85
src/utils/duration.js Normal file
View File

@@ -0,0 +1,85 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const ONE_MINUTE = 60 * 1000;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
function normalizeAge(num) {
const hundredtized = num * 100;
const isWhole = hundredtized % 100 === 0;
return isWhole ? hundredtized / 100 : num;
}
function toDoubleDigits(num) {
if (num >= 10) {
return num;
} else {
return `0${num}`;
}
}
export function getDuration(numericDuration) {
let result;
let age;
if (numericDuration > ONE_DAY - 1) {
age = normalizeAge((numericDuration / ONE_DAY)).toFixed(2);
result = `+ ${age} day`;
if (age !== 1) {
result += 's';
}
} else if (numericDuration > ONE_HOUR - 1) {
age = normalizeAge((numericDuration / ONE_HOUR).toFixed(2));
result = `+ ${age} hour`;
if (age !== 1) {
result += 's';
}
} else {
age = normalizeAge((numericDuration / ONE_MINUTE).toFixed(2));
result = `+ ${age} min`;
if (age !== 1) {
result += 's';
}
}
return result;
}
export function getPreciseDuration(numericDuration) {
let result;
const days = toDoubleDigits(Math.floor((numericDuration) / (24 * 60 * 60 * 1000)));
let remaining = (numericDuration) % (24 * 60 * 60 * 1000);
const hours = toDoubleDigits(Math.floor((remaining) / (60 * 60 * 1000)));
remaining = (remaining) % (60 * 60 * 1000);
const minutes = toDoubleDigits(Math.floor((remaining) / (60 * 1000)));
remaining = (remaining) % (60 * 1000);
const seconds = toDoubleDigits(Math.floor((remaining) / (1000)));
result = `${days}:${hours}:${minutes}:${seconds}`;
return result;
}

View File

@@ -21,6 +21,7 @@
*****************************************************************************/
import MCT from 'MCT';
let nativeFunctions = [];
let mockObjects = setMockObjects();

View File

@@ -0,0 +1,33 @@
export function mockLocalStorage() {
let store;
beforeEach(() => {
spyOn(Storage.prototype, 'getItem').and.callFake(getItem);
spyOn(Storage.prototype, 'setItem').and.callFake(setItem);
spyOn(Storage.prototype, 'removeItem').and.callFake(removeItem);
spyOn(Storage.prototype, 'clear').and.callFake(clear);
store = {};
function getItem(key) {
return store[key];
}
function setItem(key, value) {
store[key] = typeof value === 'string' ? value : JSON.stringify(value);
}
function removeItem(key) {
store[key] = undefined;
delete store[key];
}
function clear() {
store = {};
}
});
afterEach(() => {
store = undefined;
});
}