Compare commits

..

27 Commits

Author SHA1 Message Date
Andrew Henry
15926cc9a2 Merge branch 'master' into yield-request-callback 2021-09-17 17:45:00 -07:00
Shefali Joshi
dad9f12a5c Imagery view for time strip (#4114)
* Imagery view for time strip

Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2021-09-17 20:05:40 -04:00
David Tsay
aa5edb0b83 Flexible inspector properties (#4109)
* inspector properties can be passed in through context
* defaults to the currently hard-coded details (title, type, modified or created)
2021-09-17 16:20:22 -07:00
Jamie V
d647e287fd Merge branch 'master' into yield-request-callback 2021-09-17 15:56:56 -07:00
Charles Hacskaylo
b315803180 Add linear progress bar to tables (#4204)
* Add linear progress bar to tables
- Stubbed in markup;
- Better CSS anim for clarity;

* Indeterminate progress bar for telemetry tables
- Significant refinements to use ProgressBar.vue component;
- Stubbed in temp computed property for `progressLoad`;
- Better animation approach for indeterminate progress;
- Refined markup and CSS for `c-progress-bar`;

* Indeterminate progress bar for telemetry tables
- Refinements for determinate progress as shown in notification banners;
- Refined look of progress bar for clarity;
- Vue component now uses progressPerc === undefined instead of 'unknown';
- Animation tweaked;

* Changed progress-bar v-if test
- Per PR change suggestion, replace `v-if=progressLoad.progressPerc !== 100`
to `v-if=loading`;
2021-09-17 15:53:00 -07:00
Jamie V
7998ee3f61 Merge branch 'master' into yield-request-callback 2021-09-17 09:54:37 -07:00
Jamie V
e7d94d9447 Merge branch 'master' into yield-request-callback 2021-09-16 11:18:08 -07:00
Jamie Vigliotta
336babdb15 moving requestAbort undefined assignment after try/catch block, since it happens eitherway 2021-09-16 11:17:19 -07:00
Jamie Vigliotta
0b4624fc8f adding onPartialResponse back in, done testing without 2021-09-16 11:11:46 -07:00
Jamie Vigliotta
62c54e732d suppressing abort error 2021-09-16 11:11:08 -07:00
Jamie Vigliotta
e63393b0c4 looking to suppress errors related to aborting and testing reguarl requests (not yield) are still working with new updates to requests 2021-09-16 10:20:18 -07:00
Jamie Vigliotta
02c97ae10b Merge branch 'yield-request-callback' of https://github.com/nasa/openmct into yield-request-callback
Merge'n master
2021-09-15 10:43:07 -07:00
Jamie Vigliotta
5b499368ae updating key for processing method 2021-09-15 10:42:56 -07:00
Andrew Henry
dd556c3335 Merge branch 'master' into yield-request-callback 2021-09-14 17:34:20 -07:00
Jamie V
1a71708b63 Merge branch 'master' into yield-request-callback 2021-09-14 09:13:53 -07:00
Jamie V
ab425ed04e Merge branch 'master' into yield-request-callback 2021-09-09 12:30:36 -07:00
Jamie Vigliotta
514181c2fd debuggin 2021-09-08 14:15:55 -07:00
Jamie Vigliotta
757ff1c85a debuggin 2021-09-08 13:48:25 -07:00
Jamie Vigliotta
e7f21654e0 was doin generators all wrong 2021-09-08 13:04:13 -07:00
Jamie Vigliotta
c895869cbf wrapping generator function 2021-09-08 12:40:53 -07:00
Jamie Vigliotta
5d40a55a20 debugging processGenerator 2021-09-08 12:19:17 -07:00
Jamie Vigliotta
54477f2bb9 debugging processGenerator 2021-09-08 12:09:03 -07:00
Jamie Vigliotta
eb894894b5 debugging processGenerator 2021-09-08 11:59:07 -07:00
Jamie Vigliotta
3f06bc5c48 telemetry check 2021-09-08 11:29:30 -07:00
Jamie Vigliotta
c3238e50ee fixing random module auto-complete inserted 2021-09-08 11:11:29 -07:00
Jamie Vigliotta
d83e919541 fixing random module auto-complete inserted 2021-09-07 15:34:55 -07:00
Jamie Vigliotta
5fcf0bd7de added processor generator to the request options 2021-09-07 11:19:12 -07:00
29 changed files with 1554 additions and 430 deletions

View File

@@ -115,6 +115,7 @@ export class TelemetryCollection extends EventEmitter {
this._requestHistoricalTelemetry();
}
/**
* If a historical provider exists, then historical requests will be made
* @private
@@ -126,20 +127,25 @@ export class TelemetryCollection extends EventEmitter {
let historicalData;
this.options.onPartialResponse = this._processNewTelemetry.bind(this);
try {
this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal;
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
this.requestAbort = undefined;
} catch (error) {
console.error('Error requesting telemetry data...');
this.requestAbort = undefined;
this._error(error);
if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...');
this._error(error);
}
}
this.requestAbort = undefined;
this._processNewTelemetry(historicalData);
}
/**
* This uses the built in subscription function from Telemetry API
* @private

View File

@@ -0,0 +1,73 @@
/*****************************************************************************
* 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 ImageryTimeView from './components/ImageryTimeView.vue';
import Vue from "vue";
export default function ImageryTimestripViewProvider(openmct) {
const type = 'example.imagery.time-strip.view';
function hasImageTelemetry(domainObject) {
const metadata = openmct.telemetry.getMetadata(domainObject);
if (!metadata) {
return false;
}
return metadata.valuesForHints(['image']).length > 0;
}
return {
key: type,
name: 'Imagery Timestrip View',
cssClass: 'icon-image',
canView: function (domainObject, objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return hasImageTelemetry(domainObject) && isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
ImageryTimeView
},
provide: {
openmct: openmct,
domainObject: domainObject,
objectPath: objectPath
},
template: '<imagery-time-view></imagery-time-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@@ -1,4 +1,4 @@
import ImageryViewLayout from './components/ImageryViewLayout.vue';
import ImageryViewComponent from './components/ImageryView.vue';
import Vue from 'vue';
@@ -14,7 +14,7 @@ export default class ImageryView {
this.component = new Vue({
el: element,
components: {
ImageryViewLayout
'imagery-view': ImageryViewComponent
},
provide: {
openmct: this.openmct,
@@ -22,7 +22,8 @@ export default class ImageryView {
objectPath: this.objectPath,
currentView: this
},
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
template: '<imagery-view ref="ImageryContainer"></imagery-view>'
});
}

View File

@@ -37,8 +37,10 @@ export default function ImageryViewProvider(openmct) {
key: type,
name: 'Imagery Layout',
cssClass: 'icon-image',
canView: function (domainObject) {
return hasImageTelemetry(domainObject);
canView: function (domainObject, objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath));
},
view: function (domainObject, objectPath) {
return new ImageryView(openmct, domainObject, objectPath);

View File

@@ -0,0 +1,476 @@
<!--
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 ref="imagery"
class="c-imagery-tsv c-timeline-holder"
>
<div ref="imageryHolder"
class="c-imagery-tsv__contents u-contents"
>
</div>
</div>
</template>
<script>
import * as d3Scale from 'd3-scale';
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import Vue from "vue";
import imageryData from "../../imagery/mixins/imageryData";
import PreviewAction from "@/ui/preview/PreviewAction";
import _ from "lodash";
const PADDING = 1;
const RESIZE_POLL_INTERVAL = 200;
const ROW_HEIGHT = 100;
const IMAGE_WIDTH_THRESHOLD = 40;
export default {
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath'],
data() {
let timeSystem = this.openmct.time.timeSystem();
this.metadata = {};
this.requestCount = 0;
return {
viewBounds: undefined,
height: 0,
durationFormatter: undefined,
imageHistory: [],
timeSystem: timeSystem,
keyString: undefined
};
},
computed: {
imageHistorySize() {
return this.imageHistory.length;
}
},
watch: {
imageHistorySize(newSize, oldSize) {
this.updatePlotImagery();
}
},
mounted() {
this.previewAction = new PreviewAction(this.openmct);
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
this.canvas.height = 0;
this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
this.updateViewBounds();
this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery);
this.openmct.time.on("bounds", this.updateViewBounds);
this.resize = _.debounce(this.resize, 400);
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
this.imageryStripResizeObserver.observe(this.$refs.imagery);
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
if (this.imageryStripResizeObserver) {
this.imageryStripResizeObserver.disconnect();
}
this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery);
this.openmct.time.off("bounds", this.updateViewBounds);
if (this.unlisten) {
this.unlisten();
}
},
methods: {
expand(index) {
const path = this.objectPath[0];
this.previewAction.invoke([path]);
},
observeForChanges(mutatedObject) {
this.updateViewBounds();
},
resize() {
let clientWidth = this.getClientWidth();
if (clientWidth !== this.width) {
this.setDimensions();
this.updateViewBounds();
}
},
getClientWidth() {
let clientWidth = this.$refs.imagery.clientWidth;
if (!clientWidth) {
//this is a hack - need a better way to find the parent of this component
let parent = this.openmct.layout.$refs.browseObject.$el;
if (parent) {
clientWidth = parent.getBoundingClientRect().width;
}
}
return clientWidth;
},
updateViewBounds(bounds, isTick) {
this.viewBounds = this.openmct.time.bounds();
//Add a 50% padding to the end bounds to look ahead
let timespan = (this.viewBounds.end - this.viewBounds.start);
let padding = timespan / 2;
this.viewBounds.end = this.viewBounds.end + padding;
if (this.timeSystem === undefined) {
this.timeSystem = this.openmct.time.timeSystem();
}
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
},
setScaleAndPlotImagery(timeSystem, clearAllImagery) {
if (timeSystem !== undefined) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(this.timeSystem.key);
}
this.setScale(this.timeSystem);
this.updatePlotImagery(clearAllImagery);
},
getFormatter(key) {
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
let metadataValue = metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
updatePlotImagery(clearAllImagery) {
this.clearPreviousImagery(clearAllImagery);
if (this.xScale) {
this.drawImagery();
}
},
clearPreviousImagery(clearAllImagery) {
//TODO: Only clear items that are out of bounds
let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items");
noItemsEl.forEach(item => {
item.remove();
});
let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper");
imagery.forEach(item => {
if (clearAllImagery) {
item.remove();
} else {
const id = this.getNSAttributesForElement(item, 'id');
if (id) {
const timestamp = id.replace('id-', '');
if (!this.isImageryInBounds({
time: timestamp
})) {
item.remove();
}
}
}
});
},
setDimensions() {
const imageryHolder = this.$refs.imagery;
this.width = this.getClientWidth();
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
},
setScale(timeSystem) {
if (!this.width) {
return;
}
if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem();
}
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
this.xScale.domain(
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
);
} else {
this.xScale = d3Scale.scaleLinear();
this.xScale.domain(
[this.viewBounds.start, this.viewBounds.end]
);
}
this.xScale.range([PADDING, this.width - PADDING * 2]);
},
isImageryInBounds(imageObj) {
return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start);
},
getImageryContainer() {
let svgHeight = 100;
let svgWidth = this.imageHistory.length ? this.width : 200;
let groupSVG;
let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg");
if (existingSVG) {
groupSVG = existingSVG;
this.setNSAttributesForElement(groupSVG, {
width: svgWidth
});
} else {
let component = new Vue({
components: {
SwimLane
},
provide: {
openmct: this.openmct
},
data() {
return {
isNested: true,
height: svgHeight,
width: svgWidth
};
},
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><svg class="c-imagery-tsv-container" :height="height" :width="width"></svg></template></swim-lane>`
});
this.$refs.imageryHolder.appendChild(component.$mount().$el);
groupSVG = component.$el.querySelector('svg');
groupSVG.addEventListener('mouseout', (event) => {
if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') {
this.removeFromForeground();
}
});
}
return groupSVG;
},
isImageryWidthAcceptable() {
// We're calculating if there is enough space between images to show the thumbnails.
// This algorithm could probably be enhanced to check the x co-ordinate distance between 2 consecutive images, but
// we will go with this for now assuming imagery is not sorted by asc time so it's difficult to calculate.
// TODO: Use telemetry.requestCollection to get sorted telemetry
const currentStart = this.viewBounds.start;
const currentEnd = this.viewBounds.end;
const rectX = this.xScale(currentStart);
const rectY = this.xScale(currentEnd);
const imageContainerWidth = this.imageHistory.length ? (rectY - rectX) / this.imageHistory.length : 0;
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
},
drawImagery() {
let groupSVG = this.getImageryContainer();
const showImagePlaceholders = this.isImageryWidthAcceptable();
if (this.imageHistory.length) {
this.imageHistory.forEach((currentImageObject, index) => {
if (this.isImageryInBounds(currentImageObject)) {
this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index);
}
});
} else {
this.plotNoItems(groupSVG);
}
},
plotNoItems(svgElement) {
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
this.setNSAttributesForElement(textElement, {
x: "10",
y: "20",
class: "c-imagery-tsv__no-items"
});
textElement.innerHTML = 'No images within timeframe';
svgElement.appendChild(textElement);
},
setNSAttributesForElement(element, attributes) {
Object.keys(attributes).forEach((key) => {
if (key === 'url') {
element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]);
} else {
element.setAttributeNS(null, key, attributes[key]);
}
});
},
getNSAttributesForElement(element, attribute) {
return element.getAttributeNS(null, attribute);
},
getImageWrapper(item) {
const id = `id-${item.time}`;
return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`);
},
plotImagery(item, showImagePlaceholders, svgElement, index) {
//TODO: Placeholder image
let existingImageWrapper = this.getImageWrapper(item);
//imageWrapper wraps the vertical tick rect and the image
if (existingImageWrapper) {
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
} else {
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement);
svgElement.appendChild(imageWrapper);
}
},
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
//Update the x co-ordinates of the handle and image elements and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
showImagePlaceholders
});
let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle');
this.setNSAttributesForElement(imageTickElement, {
x: this.xScale(item.time)
});
let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder');
this.setNSAttributesForElement(imageRect, {
x: this.xScale(item.time) + 2
});
let imageElement = existingImageWrapper.querySelector('image');
const selector = `href*=${existingImageWrapper.id}`;
let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`);
const hideImageUrl = (showImagePlaceholders && !hoverEl);
this.setNSAttributesForElement(imageElement, {
x: this.xScale(item.time) + 2,
url: hideImageUrl ? '' : item.url
});
},
createImageWrapper(index, item, showImagePlaceholders, svgElement) {
const id = `id-${item.time}`;
const imgSize = String(ROW_HEIGHT - 15);
let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.setNSAttributesForElement(imageWrapper, {
id,
class: 'c-imagery-tsv__image-wrapper',
showImagePlaceholders
});
//create image tick indicator
let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageTickElement, {
class: 'c-imagery-tsv__image-handle',
x: this.xScale(item.time),
y: 5,
rx: 0,
width: 2,
height: String(ROW_HEIGHT - 10)
});
imageWrapper.appendChild(imageTickElement);
let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageRect, {
class: 'c-imagery-tsv__image-placeholder',
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
mask: `#image-${item.time}`
});
imageWrapper.appendChild(imageRect);
let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image');
this.setNSAttributesForElement(imageElement, {
id: `image-${item.time}`,
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
url: showImagePlaceholders ? '' : item.url
});
imageWrapper.appendChild(imageElement);
//TODO: Don't add the hover listener if the width is too small
imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url));
return imageWrapper;
},
bringToForeground(svgElement, imageWrapper, index, url, event) {
const selector = `href*=${imageWrapper.id}`;
let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`);
if (hoverEls.length > 0) {
this.removeFromForeground(hoverEls);
}
hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`);
if (hoverEls.length) {
return;
}
let imageElement = imageWrapper.querySelector('image');
this.setNSAttributesForElement(imageElement, {
url: url,
fill: 'none'
});
let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use');
this.setNSAttributesForElement(hoverElement, {
class: 'image-highlight',
x: 0,
href: `#${imageWrapper.id}`
});
this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper is-hovered'
});
// We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably
hoverElement.addEventListener('mousedown', (e) => {
if (e.button === 0) {
this.expand(index);
}
});
svgElement.appendChild(hoverElement);
},
removeFromForeground(items) {
let hoverEls;
if (items) {
hoverEls = items;
} else {
hoverEls = this.$el.querySelectorAll(".c-imagery-tsv__contents use");
}
hoverEls.forEach(item => {
let selector = `id*=${this.getNSAttributesForElement(item, 'href').replace('#', '')}`;
let imageWrapper = this.$el.querySelector(`.c-imagery-tsv__contents g[${selector}]`);
this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper'
});
let showImagePlaceholders = this.getNSAttributesForElement(imageWrapper, 'showImagePlaceholders');
if (showImagePlaceholders === 'true') {
let imageElement = imageWrapper.querySelector('image');
this.setNSAttributesForElement(imageElement, {
url: ''
});
}
item.remove();
});
}
}
};
</script>

View File

@@ -129,12 +129,11 @@
</div>
</div>
</div>
<div
class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
<div class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
>
<div
ref="thumbsWrapper"
@@ -175,7 +174,8 @@ import moment from 'moment';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
import imageryData from "../../imagery/mixins/imageryData";
const REFRESH_CSS_MS = 500;
const DURATION_TRACK_MS = 1000;
const ARROW_DOWN_DELAY_CHECK_MS = 400;
@@ -197,30 +197,29 @@ export default {
components: {
Compass
},
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
data() {
let timeSystem = this.openmct.time.timeSystem();
this.metadata = {};
this.requestCount = 0;
return {
autoScroll: true,
durationFormatter: undefined,
imageHistory: [],
timeSystem: timeSystem,
keyString: undefined,
autoScroll: true,
filters: {
brightness: 100,
contrast: 100
},
imageHistory: [],
thumbnailClick: THUMBNAIL_CLICKED,
isPaused: false,
metadata: {},
requestCount: 0,
timeSystem: timeSystem,
timeFormatter: undefined,
refreshCSS: false,
keyString: undefined,
focusedImageIndex: undefined,
focusedImageRelatedTelemetry: {},
numericDuration: undefined,
metadataEndpoints: {},
relatedTelemetry: {},
latestRelatedTelemetry: {},
focusedImageNaturalAspectRatio: undefined,
@@ -231,6 +230,9 @@ export default {
};
},
computed: {
imageHistorySize() {
return this.imageHistory.length;
},
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageDimensions.width < 300) {
@@ -258,9 +260,6 @@ export default {
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
},
focusedImageDownloadName() {
return this.getImageDownloadName(this.focusedImage);
},
isNextDisabled() {
let disabled = false;
@@ -383,6 +382,10 @@ export default {
}
},
watch: {
imageHistorySize(newSize, oldSize) {
this.setFocusedImage(newSize - 1, false);
this.scrollToRight();
},
focusedImageIndex() {
this.trackDuration();
this.resetAgeCSS();
@@ -391,18 +394,9 @@ export default {
}
},
async mounted() {
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
this.openmct.time.on('clock', this.clockChange);
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
//listen
this.openmct.time.on('timeSystem', this.trackDuration);
this.openmct.time.on('clock', this.trackDuration);
// related telemetry keys
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
@@ -410,56 +404,49 @@ export default {
this.cameraKeys = ['cameraPan', 'cameraTilt'];
this.sunKeys = ['sunOrientation'];
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.requestHistory();
// related telemetry
await this.initializeRelatedTelemetry();
this.updateRelatedTelemetryForFocusedImage();
await this.updateRelatedTelemetryForFocusedImage();
this.trackLatestRelatedTelemetry();
// for scrolling through images quickly and resizing the object view
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
_.debounce(this.resizeImageContainer, 400);
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
// for resizing the object view
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
if (this.$refs.imageBG) {
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
}
// For adjusting scroll bar size and position when resizing thumbs wrapper
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
if (this.$refs.thumbsWrapper) {
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
}
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
this.openmct.time.off('timeSystem', this.trackDuration);
this.openmct.time.off('clock', this.trackDuration);
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.imageContainerResizeObserver) {
this.imageContainerResizeObserver.disconnect();
}
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
}
this.stopDurationTracking();
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
this.openmct.time.off('clock', this.clockChange);
// unsubscribe from related telemetry
if (this.relatedTelemetry.hasRelatedTelemetry) {
for (let key of this.relatedTelemetry.keys) {
@@ -576,56 +563,6 @@ export default {
focusElement() {
this.$el.focus();
},
datumIsNotValid(datum) {
if (this.imageHistory.length === 0) {
return false;
}
const datumURL = this.formatImageUrl(datum);
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history
const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale;
},
formatImageUrl(datum) {
if (!datum) {
return;
}
return this.imageFormatter.format(datum);
},
formatTime(datum) {
if (!datum) {
return;
}
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
},
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
const key = this.imageDownloadNameHints.key;
imageDownloadName = datum[key];
}
return imageDownloadName;
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper || this.resizingWindow) {
@@ -683,6 +620,10 @@ export default {
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
this.nextImageIndex = index;
//this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) {
this.focusedImageIndex = index;
}
return;
}
@@ -693,70 +634,6 @@ export default {
this.paused(true);
}
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.requestHistory();
}
},
async requestHistory() {
let bounds = this.openmct.time.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
let data = await this.openmct.telemetry
.request(this.domainObject, bounds) || [];
if (this.requestCount === requestId) {
data.forEach((datum, index) => {
this.updateHistory(datum, index === data.length - 1);
});
}
},
timeSystemChange(system) {
this.timeSystem = this.openmct.time.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.trackDuration();
},
clockChange(clock) {
this.trackDuration();
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
this.updateHistory(datum);
}
});
},
updateHistory(datum, setFocused = true) {
if (this.datumIsNotValid(datum)) {
return;
}
let image = { ...datum };
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey];
image.imageDownloadName = this.getImageDownloadName(datum);
this.imageHistory.push(image);
if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1);
this.scrollToRight();
}
},
getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
trackDuration() {
if (this.canTrackDuration) {
this.stopDurationTracking();
@@ -876,6 +753,10 @@ export default {
}, { once: true });
},
resizeImageContainer() {
if (!this.$refs.imageBG) {
return;
}
if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.imageBG.clientWidth;
}

View File

@@ -312,3 +312,34 @@
@include cArrowButtonSizing($dimOuter: 32px);
}
}
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
.c-imagery-tsv {
g.c-imagery-tsv__image-wrapper {
cursor: pointer;
&.is-hovered {
filter: brightness(1) contrast(1) !important;
[class*='__image-handle'] {
fill: $colorBodyFg;
}
}
}
&__no-items {
fill: $colorBodyFg !important;
}
&__image-handle {
fill: rgba($colorBodyFg, 0.5);
}
&__image-placeholder {
fill: pushBack($colorBodyBg, 0.3);
}
&:hover g.c-imagery-tsv__image-wrapper {
// TODO CH: convert to theme constants
filter: brightness(0.5) contrast(0.7);
}
}

View File

@@ -0,0 +1,174 @@
/*****************************************************************************
* 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 DEFAULT_DURATION_FORMATTER = 'duration';
export default {
inject: ['openmct', 'domainObject', 'objectPath'],
mounted() {
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.requestHistory();
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
},
methods: {
datumIsNotValid(datum) {
if (this.imageHistory.length === 0) {
return false;
}
const datumURL = this.formatImageUrl(datum);
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history
const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale;
},
formatImageUrl(datum) {
if (!datum) {
return;
}
return this.imageFormatter.format(datum);
},
formatTime(datum) {
if (!datum) {
return;
}
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
},
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
const key = this.imageDownloadNameHints.key;
imageDownloadName = datum[key];
}
return imageDownloadName;
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.requestHistory();
}
},
async requestHistory() {
let bounds = this.openmct.time.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
let data = await this.openmct.telemetry
.request(this.domainObject, bounds) || [];
if (this.requestCount === requestId) {
let imagery = [];
data.forEach((datum) => {
let image = this.normalizeDatum(datum);
if (image) {
imagery.push(image);
}
});
//this is to optimize anything that reacts to imageHistory length
this.imageHistory = imagery;
}
},
timeSystemChange() {
this.timeSystem = this.openmct.time.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
let image = this.normalizeDatum(datum);
if (image) {
this.imageHistory.push(image);
}
}
});
},
normalizeDatum(datum) {
if (this.datumIsNotValid(datum)) {
return;
}
let image = { ...datum };
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey];
image.imageDownloadName = this.getImageDownloadName(datum);
return image;
},
getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
}
}
};

View File

@@ -21,10 +21,12 @@
*****************************************************************************/
import ImageryViewProvider from './ImageryViewProvider';
import ImageryTimestripViewProvider from './ImageryTimestripViewProvider';
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new ImageryViewProvider(openmct));
openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct));
};
}

View File

@@ -84,12 +84,14 @@ function generateTelemetry(start, count) {
return telemetry;
}
describe("The Imagery View Layout", () => {
describe("The Imagery View Layouts", () => {
const imageryKey = 'example.imagery';
const imageryForTimeStripKey = 'example.imagery.time-strip.view';
const START = Date.now();
const COUNT = 10;
let resolveFunction;
let originalRouterPath;
let openmct;
let appHolder;
@@ -116,51 +118,51 @@ describe("The Imagery View Layout", () => {
"image": 1,
"priority": 3
},
"source": "url",
"relatedTelemetry": {
"heading": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "heading",
"valueKey": "value"
}
},
"roll": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "roll",
"valueKey": "value"
}
},
"pitch": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "pitch",
"valueKey": "value"
}
},
"cameraPan": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "cameraPan",
"valueKey": "value"
}
},
"cameraTilt": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "cameraTilt",
"valueKey": "value"
}
},
"sunOrientation": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "sunOrientation",
"valueKey": "value"
}
}
}
"source": "url"
// "relatedTelemetry": {
// "heading": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "heading",
// "valueKey": "value"
// }
// },
// "roll": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "roll",
// "valueKey": "value"
// }
// },
// "pitch": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "pitch",
// "valueKey": "value"
// }
// },
// "cameraPan": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "cameraPan",
// "valueKey": "value"
// }
// },
// "cameraTilt": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "cameraTilt",
// "valueKey": "value"
// }
// },
// "sunOrientation": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "sunOrientation",
// "valueKey": "value"
// }
// }
// }
},
{
"name": "Name",
@@ -220,6 +222,8 @@ describe("The Imagery View Layout", () => {
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
originalRouterPath = openmct.router.path;
openmct.on('start', done);
openmct.start(appHolder);
});
@@ -229,10 +233,34 @@ describe("The Imagery View Layout", () => {
start: 0,
end: 1
});
openmct.router.path = originalRouterPath;
return resetApplicationState(openmct);
});
it("should provide an imagery time strip view when in a time strip", () => {
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryForTimeStripKey
);
expect(imageryView).toBeDefined();
});
it("should provide an imagery view only for imagery producing objects", () => {
let applicableViews = openmct.objectViews.get(imageryObject, []);
let imageryView = applicableViews.find(
@@ -242,6 +270,46 @@ describe("The Imagery View Layout", () => {
expect(imageryView).toBeDefined();
});
it("should not provide an imagery view when in a time strip", () => {
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
expect(imageryView).toBeUndefined();
});
it("should provide an imagery view when navigated to in the composition of a time strip", () => {
openmct.router.path = [imageryObject];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
expect(imageryView).toBeDefined();
});
describe("imagery view", () => {
let applicableViews;
let imageryViewProvider;
@@ -367,18 +435,18 @@ describe("The Imagery View Layout", () => {
});
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
imageryView._getInstance().$refs.ImageryContainer.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;
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset');
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
});
});

View File

@@ -143,10 +143,12 @@ export default {
},
updateViewBounds() {
this.viewBounds = this.openmct.time.bounds();
if (!this.options.compact) {
//Add a 50% padding to the end bounds to look ahead
let timespan = (this.viewBounds.end - this.viewBounds.start);
let padding = timespan / 2;
this.viewBounds.end = this.viewBounds.end + padding;
let timespan = (this.viewBounds.end - this.viewBounds.start);
let padding = timespan / 2;
this.viewBounds.end = this.viewBounds.end + padding;
}
if (this.timeSystem === undefined) {
this.timeSystem = this.openmct.time.timeSystem();

View File

@@ -34,6 +34,7 @@
class="u-contents"
:default-object="item.domainObject"
:object-path="item.objectPath"
@change-action-collection="setActionCollection"
/>
</swim-lane>
</template>
@@ -106,6 +107,9 @@ export default {
this.$el, this.context);
}
});
},
setActionCollection(actionCollection) {
this.openmct.menus.actionsToMenuItems(actionCollection.getVisibleActions(), actionCollection.objectPath, actionCollection.view);
}
}
};

View File

@@ -53,7 +53,7 @@ export default class ViewLargeAction {
const parentElement = view.parentElement;
const element = parentElement && parentElement.firstChild;
const viewLargeAction = element && !element.classList.contains('js-main-container')
&& !this._isNavigatedObject(objectPath);
&& !this.openmct.router.isNavigatedObject(objectPath);
return viewLargeAction;
}
@@ -85,11 +85,4 @@ export default class ViewLargeAction {
return preview.$mount().$el;
}
_isNavigatedObject(objectPath) {
let targetObject = objectPath[0];
let navigatedObject = this.openmct.router.path[0];
return this.openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier);
}
}

View File

@@ -16,7 +16,7 @@
@import "../plugins/folderView/components/grid-view.scss";
@import "../plugins/folderView/components/list-item.scss";
@import "../plugins/folderView/components/list-view.scss";
@import "../plugins/imagery/components/imagery-view-layout.scss";
@import "../plugins/imagery/components/imagery-view.scss";
@import "../plugins/imagery/components/Compass/compass.scss";
@import "../plugins/telemetryTable/components/table-row.scss";
@import "../plugins/telemetryTable/components/table-footer-indicator.scss";

View File

@@ -122,8 +122,10 @@ export default {
}
},
drawAxis(bounds, timeSystem) {
this.setScale(bounds, timeSystem);
this.setAxis(bounds);
let viewBounds = Object.assign({}, bounds);
this.setScale(viewBounds, timeSystem);
this.setAxis(viewBounds);
this.axisElement.call(this.xAxis);
this.updateNowMarker();

View File

@@ -3,7 +3,8 @@
:class="{'c-swimlane': !isNested}"
>
<div class="c-swimlane__lane-label c-object-label"
<div v-if="hideLabel === false"
class="c-swimlane__lane-label c-object-label"
:class="{'c-swimlane__lane-label--span-cols': (!spanRowsCount && !isNested)}"
:style="gridRowSpan"
>
@@ -49,6 +50,12 @@ export default {
return false;
}
},
hideLabel: {
type: Boolean,
default() {
return false;
}
},
isNested: {
type: Boolean,
default() {

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>
@@ -57,7 +81,7 @@ import multipane from '../layout/multipane.vue';
import pane from '../layout/pane.vue';
import ElementsPool from './ElementsPool.vue';
import Location from './Location.vue';
import Properties from './Properties.vue';
import Properties from './details/Properties.vue';
import ObjectName from './ObjectName.vue';
import InspectorViews from './InspectorViews.vue';
import _ from "lodash";
@@ -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,166 @@
/*****************************************************************************
* 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 {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import Vue from 'vue';
const INSPECTOR_SELECTOR_PREFIX = '.c-inspect-properties__';
describe('the inspector', () => {
let appHolder;
let openmct;
let folderItem;
let selection;
beforeEach((done) => {
folderItem = {
name: 'folder',
type: 'folder',
id: 'mock-folder-key',
identifier: {
namespace: '',
key: 'mock-folder-key'
},
created: 1592851063871
};
selection = [
{
context: {
item: folderItem
}
}
];
appHolder = document.createElement('div');
openmct = createOpenMct();
openmct.on('start', done);
openmct.start(appHolder);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('displays default details for selection', async () => {
openmct.selection.select(selection);
await Vue.nextTick();
const details = getDetails();
const [
title,
type,
timestamp
] = details;
expect(title.name)
.toEqual('Title');
expect(title.value.toLowerCase())
.toEqual(folderItem.name);
expect(type.name)
.toEqual('Type');
expect(type.value.toLowerCase())
.toEqual(folderItem.type);
expect(timestamp.name)
.toEqual('Created');
expect(new Date(timestamp.value).toString())
.toEqual(new Date(folderItem.created).toString());
});
it('details show modified date', async () => {
const modifiedTimestamp = folderItem.created + 1000;
folderItem.modified = modifiedTimestamp;
openmct.selection.select(selection);
await Vue.nextTick();
const details = getDetails();
const timestamp = details[details.length - 1];
expect(timestamp.name)
.toEqual('Modified');
expect(new Date(timestamp.value).toString())
.toEqual(new Date(folderItem.modified).toString());
});
it('displays custom details if provided through context', async () => {
const NAME_PREFIX = 'Custom Name';
const VALUE_PREFIX = 'Custom Value';
const indexes = [0, 1, 2, 3, 4, 5, 6, 7, 8];
const customDetails = indexes.map(index => {
return {
name: `${NAME_PREFIX} ${index}`,
value: `${VALUE_PREFIX} ${index}`
};
});
selection[0].context.details = customDetails;
openmct.selection.select(selection);
await Vue.nextTick();
const details = getDetails();
expect(details.length)
.toEqual(customDetails.length);
details.forEach((detail, index) => {
expect(detail.name)
.toEqual(customDetails[index].name);
expect(detail.value)
.toEqual(customDetails[index].value);
});
});
function getDetailsElements() {
const inspectorDetailsSection = appHolder
.querySelector(`${INSPECTOR_SELECTOR_PREFIX}section`);
const details = inspectorDetailsSection
.querySelectorAll(`${INSPECTOR_SELECTOR_PREFIX}row`);
return details;
}
function getDetails() {
const detailsElements = getDetailsElements();
const details = Array.from(detailsElements)
.map(element => {
return {
name: getText(element, 'label'),
value: getText(element, 'value')
};
});
return details;
}
function getText(element, selectorSuffix) {
return element.querySelector(`${INSPECTOR_SELECTOR_PREFIX}${selectorSuffix}`)
.textContent.trim();
}
});

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 const mockTelemetryTableSelection = [
[{
context: {

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"

View File

@@ -1,171 +0,0 @@
<template>
<div v-if="!activity"
class="c-inspector__properties c-inspect-properties"
>
<div class="c-inspect-properties__header">
Details
</div>
<ul
v-if="!multiSelect && !singleSelectNonObject"
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>
</ul>
<div
v-if="multiSelect"
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
</div>
</div>
</template>
<script>
import Moment from "moment";
export default {
inject: ['openmct'],
data() {
return {
domainObject: {},
activity: undefined,
multiSelect: false
};
},
computed: {
item() {
return this.domainObject || {};
},
type() {
return this.openmct.types.get(this.item.type);
},
typeName() {
if (!this.type) {
return `Unknown: ${this.item.type}`;
}
return this.type.definition.name;
},
typeProperties() {
if (!this.type) {
return [];
}
let definition = this.type.definition;
if (!definition.form || definition.form.length === 0) {
return [];
}
return definition.form
.map((field) => {
let path = field.property;
if (typeof path === 'string') {
path = [path];
}
return {
name: field.name,
path
};
})
.filter(field => Array.isArray(field.path))
.map((field) => {
return {
name: field.name,
value: field.path.reduce((object, key) => {
return object[key];
}, this.item)
};
});
},
singleSelectNonObject() {
return !this.item.identifier && !this.multiSelect;
}
},
mounted() {
this.openmct.selection.on('change', this.updateSelection);
this.updateSelection(this.openmct.selection.get());
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
},
methods: {
updateSelection(selection) {
if (selection.length === 0 || selection[0].length === 0) {
this.domainObject = {};
return;
}
if (selection.length > 1) {
this.multiSelect = true;
this.domainObject = {};
return;
} else {
this.multiSelect = false;
this.domainObject = selection[0][0].context.item;
this.activity = selection[0][0].context.activity;
}
},
formatTime(unixTime) {
return Moment.utc(unixTime).format('YYYY-MM-DD[\n]HH:mm:ss') + ' UTC';
}
}
};
</script>

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,201 @@
/*****************************************************************************
* 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="hasDetails"
class="c-inspect-properties__section"
>
<Component
:is="getComponent(detail)"
v-for="detail in details"
:key="detail.name"
:detail="detail"
/>
</ul>
<div
v-else
class="c-inspect-properties__row--span-all"
>
{{ noDetailsMessage }}
</div>
</div>
</template>
<script>
import Moment from 'moment';
import DetailText from './DetailText.vue';
export default {
components: {
DetailText
},
inject: ['openmct'],
data() {
return {
selection: undefined
};
},
computed: {
details() {
return this.customDetails ? this.customDetails : this.domainObjectDetails;
},
customDetails() {
if (this.context === undefined) {
return;
}
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) {
const formattedTimestamp = Moment.utc(timestamp)
.format('YYYY-MM-DD[\n]HH:mm:ss')
+ ' UTC';
details.push(
{
name: timestampLabel,
value: formattedTimestamp
}
);
}
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) {
return [];
}
let definition = this.type.definition;
if (!definition.form || definition.form.length === 0) {
return [];
}
return definition.form
.map((field) => {
let path = field.property;
if (typeof path === 'string') {
path = [path];
}
return {
name: field.name,
path
};
})
.filter(field => Array.isArray(field.path))
.map((field) => {
return {
name: field.name,
value: field.path.reduce((object, key) => {
return object[key];
}, this.domainObject)
};
});
}
},
mounted() {
this.openmct.selection.on('change', this.updateSelection);
this.updateSelection(this.openmct.selection.get());
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
},
methods: {
getComponent(detail) {
const component = detail.component ? detail.component : 'text';
return `detail-${component}`;
},
updateSelection(selection) {
this.selection = selection;
}
}
};
</script>

View File

@@ -81,17 +81,10 @@ export default class PreviewAction {
const isObjectView = parentElement && parentElement.classList.contains('js-object-view');
return !PreviewAction.isVisible
&& !this._isNavigatedObject(objectPath)
&& !this._openmct.router.isNavigatedObject(objectPath)
&& !isObjectView;
}
_isNavigatedObject(objectPath) {
let targetObject = objectPath[0];
let navigatedObject = this._openmct.router.path[0];
return this._openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier);
}
_preventPreview(objectPath) {
const noPreviewTypes = ['folder'];

View File

@@ -136,6 +136,13 @@ class ApplicationRouter extends EventEmitter {
this.handleLocationChange(hash.substring(1));
}
isNavigatedObject(objectPath) {
let targetObject = objectPath[0];
let navigatedObject = this.path[0];
return this.openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier);
}
/**
* Add routes listeners
*