Merge 1.8.1 into master (#4562)

* Transaction fix (#4421) (#4461)
* When transaction is active, objects.get should search in dirty object first.
* Bugfix/create tree node (#4472)
* Transaction fix (#4421)
* When transaction is active, objects.get should search in dirty object first.
* find insert location prior to adding item to tree
* no need to resort
* add item should only add to direct descendants
* remove unused function
* copy composition before sorting
* remove unused var
* remove master pollution
* Revert "remove master pollution"
* add item to correct location
* Changed descending to ascending in sort order method (#4480)
* adding RAF to display layout alphanumerics (#4486)
* [Tabs] Sizing of offscreen tabs causing issues (#4444)
* [LAD Tables] Use RAF for updating template (#4500)
* Fixes LAD rows for string telemetry (#4508)
* Fixes LAD rows for string telemetry
* saving the object if it was missing (#4471)
* 4328 - Maintain reference to a focusedImage if the bounds change (#4545)
* WIP: adding assertions to catch negative index state
* just testing the flow
* SUpdate the image history index to previous selected image
* Cleaning up spacing and log statements
* Converted focusedImageIndex assignment to ternary and general cleanup
* imported objects are not persisting  (#4477)
* imported objects are not persisting #4470
* disabled karma spec reporter suppressErrorSummary
* update version number
* Delete importFromJsonAction directory since it was empty
This commit is contained in:
Shefali Joshi 2021-12-13 11:19:54 -08:00 committed by GitHub
parent 7c4258d720
commit bba29b083f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 253 additions and 88 deletions

View File

@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "1.8.1-SNAPSHOT",
"version": "1.8.1",
"description": "The Open MCT core platform",
"devDependencies": {
"@braintree/sanitize-url": "^5.0.2",

View File

@ -48,6 +48,7 @@ const CONTEXT_MENU_ACTIONS = [
'viewHistoricalData',
'remove'
];
const BLANK_VALUE = '---';
export default {
inject: ['openmct', 'currentView'],
@ -67,15 +68,43 @@ export default {
},
data() {
return {
datum: undefined,
timestamp: undefined,
value: '---',
valueClass: '',
timestampKey: undefined,
unit: ''
};
},
computed: {
value() {
if (!this.datum) {
return BLANK_VALUE;
}
return this.formats[this.valueKey].format(this.datum);
},
valueClass() {
if (!this.datum) {
return '';
}
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
return limit ? limit.cssClass : '';
},
formattedTimestamp() {
return this.timestamp !== undefined ? this.getFormattedTimestamp(this.timestamp) : '---';
if (!this.timestamp) {
return BLANK_VALUE;
}
return this.timeSystemFormat.format(this.timestamp);
},
timeSystemFormat() {
if (!this.formats[this.timestampKey]) {
console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`);
}
return this.formats[this.timestampKey];
},
objectPath() {
return [this.domainObject, ...this.pathToTable];
@ -96,15 +125,19 @@ export default {
this.timestampKey = this.openmct.time.timeSystem().key;
this.valueMetadata = this.metadata ? this
.metadata
.valuesForHints(['range'])[0] : undefined;
this.valueMetadata = undefined;
if (this.metadata) {
this.valueMetadata = this
.metadata
.valuesForHints(['range'])[0] || this.firstNonDomainAttribute(this.metadata);
}
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct
.telemetry
.subscribe(this.domainObject, this.updateValues);
.subscribe(this.domainObject, this.setLatestValues);
this.requestHistory();
@ -118,29 +151,29 @@ export default {
this.openmct.time.off('bounds', this.updateBounds);
},
methods: {
updateValues(datum) {
let newTimestamp = this.getParsedTimestamp(datum);
let limit;
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
if (this.shouldUpdate(newTimestamp)) {
this.datum = datum;
this.timestamp = newTimestamp;
this.value = this.formats[this.valueKey].format(datum);
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
if (limit) {
this.valueClass = limit.cssClass;
} else {
this.valueClass = '';
}
if (this.shouldUpdate(newTimestamp)) {
this.timestamp = newTimestamp;
this.datum = this.latestDatum;
}
this.updatingView = false;
});
}
},
shouldUpdate(newTimestamp) {
let newTimestampInBounds = this.inBounds(newTimestamp);
let noExistingTimestamp = this.timestamp === undefined;
let newTimestampIsLatest = newTimestamp > this.timestamp;
setLatestValues(datum) {
this.latestDatum = datum;
return newTimestampInBounds
&& (noExistingTimestamp || newTimestampIsLatest);
this.updateView();
},
shouldUpdate(newTimestamp) {
return this.inBounds(newTimestamp)
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
},
requestHistory() {
this.openmct
@ -151,7 +184,7 @@ export default {
size: 1,
strategy: 'latest'
})
.then((array) => this.updateValues(array[array.length - 1]))
.then((array) => this.setLatestValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
@ -189,31 +222,21 @@ export default {
}
},
resetValues() {
this.value = '---';
this.timestamp = undefined;
this.valueClass = '';
this.datum = undefined;
},
getParsedTimestamp(timestamp) {
if (this.timeSystemFormat()) {
return this.formats[this.timestampKey].parse(timestamp);
}
},
getFormattedTimestamp(timestamp) {
if (this.timeSystemFormat()) {
return this.formats[this.timestampKey].format(timestamp);
}
},
timeSystemFormat() {
if (this.formats[this.timestampKey]) {
return true;
} else {
console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`);
return false;
if (this.timeSystemFormat) {
return this.timeSystemFormat.parse(timestamp);
}
},
setUnit() {
this.unit = this.valueMetadata.unit || '';
},
firstNonDomainAttribute(metadata) {
return metadata
.values()
.find(metadatum => metadatum.hints.domain === undefined && metadatum.key !== 'name');
}
}
};

View File

@ -26,6 +26,7 @@ import {
getMockObjects,
getMockTelemetry,
getLatestTelemetry,
spyOnBuiltins,
resetApplicationState
} from 'utils/testing';
@ -160,6 +161,11 @@ describe("The LAD Table", () => {
anotherTelemetryObjectResolve = resolve;
});
spyOnBuiltins(['requestAnimationFrame']);
window.requestAnimationFrame.and.callFake((callBack) => {
callBack();
});
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);

View File

@ -263,7 +263,8 @@ export default {
this.openmct.telemetry.request(this.domainObject, options)
.then(data => {
if (data.length > 0) {
this.updateView(data[data.length - 1]);
this.latestDatum = data[data.length - 1];
this.updateView();
}
});
},
@ -275,12 +276,19 @@ export default {
|| (datumTimeStamp
&& (this.openmct.time.bounds().end >= datumTimeStamp))
) {
this.updateView(datum);
this.latestDatum = datum;
this.updateView();
}
}.bind(this));
},
updateView(datum) {
this.datum = datum;
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
this.datum = this.latestDatum;
this.updatingView = false;
});
}
},
removeSubscription() {
if (this.subscription) {
@ -290,7 +298,8 @@ export default {
},
refreshData(bounds, isTick) {
if (!isTick) {
this.datum = undefined;
this.latestDatum = undefined;
this.updateView();
this.requestHistoricalData(this.domainObject);
}
},

View File

@ -414,7 +414,7 @@ export default {
if (this.indexForFocusedImage !== undefined) {
imageIndex = this.initFocusedImageIndex;
} else {
imageIndex = newSize - 1;
imageIndex = newSize > 0 ? newSize - 1 : undefined;
}
this.setFocusedImage(imageIndex, false);
@ -510,6 +510,12 @@ export default {
this.timeContext.off("timeContext", this.setTimeContext);
}
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.previousFocusedImage = this.focusedImage ? JSON.parse(JSON.stringify(this.focusedImage)) : undefined;
this.requestHistory();
}
},
expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions();
@ -670,23 +676,47 @@ export default {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
},
matchIndexOfPreviousImage(previous, imageHistory) {
// match logic uses a composite of url and time to account
// for example imagery not having fully unique urls
return imageHistory.findIndex((x) => (
x.url === previous.url
&& x.time === previous.time
));
},
setFocusedImage(index, thumbnailClick = false) {
let focusedIndex = index;
if (!(Number.isInteger(index) && index > -1)) {
return;
}
if (this.previousFocusedImage) {
// determine if the previous image exists in the new bounds of imageHistory
const matchIndex = this.matchIndexOfPreviousImage(
this.previousFocusedImage,
this.imageHistory
);
focusedIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
delete this.previousFocusedImage;
}
if (thumbnailClick) {
//We use the props till the user changes what they want to see
this.initFocusedImageIndex = undefined;
}
if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) {
this.nextImageIndex = index;
this.nextImageIndex = focusedIndex;
//this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) {
this.focusedImageIndex = index;
this.focusedImageIndex = focusedIndex;
}
return;
}
this.focusedImageIndex = index;
this.focusedImageIndex = focusedIndex;
if (thumbnailClick && !this.isPaused) {
this.paused(true);

View File

@ -31,8 +31,6 @@
flex-direction: column;
&--hidden {
height: 1000px;
width: 1000px;
position: absolute;
left: -9999px;
top: -9999px;

View File

@ -1,6 +1,10 @@
<template>
<div class="c-tabs-view">
<div
ref="tabs"
class="c-tabs-view"
>
<div
ref="tabsHolder"
class="c-tabs-view__tabs-holder c-tabs"
:class="{
'is-dragging': isDragging && allowEditing,
@ -28,8 +32,10 @@
}"
@click="showTab(tab, index)"
>
<div class="c-tabs-view__tab__label c-object-label"
:class="[tab.status ? `is-status--${tab.status}` : '']"
<div
ref="tabsLabel"
class="c-tabs-view__tab__label c-object-label"
:class="[tab.status ? `is-status--${tab.status}` : '']"
>
<div class="c-object-label__type-icon"
:class="tab.type.definition.cssClass"
@ -49,11 +55,12 @@
<div
v-for="tab in tabsList"
:key="tab.keyString"
:style="getTabStyles(tab)"
class="c-tabs-view__object-holder"
:class="{'c-tabs-view__object-holder--hidden': !isCurrent(tab)}"
>
<object-view
v-if="isTabLoaded(tab)"
v-if="shouldLoadTab(tab)"
class="c-tabs-view__object"
:default-object="tab.domainObject"
:object-path="tab.objectPath"
@ -65,6 +72,7 @@
<script>
import ObjectView from '../../../ui/components/ObjectView.vue';
import RemoveAction from '../../remove/RemoveAction.js';
import _ from 'lodash';
const unknownObjectType = {
definition: {
@ -88,6 +96,8 @@ export default {
let keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return {
tabWidth: undefined,
tabHeight: undefined,
internalDomainObject: this.domainObject,
currentTab: {},
currentTabIndex: undefined,
@ -122,6 +132,10 @@ export default {
});
}
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
this.tabsViewResizeObserver = new ResizeObserver(this.handleWindowResize);
this.tabsViewResizeObserver.observe(this.$refs.tabs);
this.unsubscribe = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.openmct.router.on('change:params', this.updateCurrentTab.bind(this));
@ -138,6 +152,8 @@ export default {
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.onReorder);
this.tabsViewResizeObserver.disconnect();
this.tabsList.forEach(tab => {
tab.statusUnsubscribe();
});
@ -158,12 +174,28 @@ export default {
this.loadedTabs[tab.keyString] = true;
},
getTabStyles(tab) {
let styles = {};
if (!this.isCurrent(tab)) {
styles = {
height: this.tabHeight,
width: this.tabWidth
};
}
return styles;
},
setCurrentTabByIndex(index) {
if (this.tabsList[index]) {
this.showTab(this.tabsList[index]);
}
},
showTab(tab, index) {
if (!tab) {
return;
}
if (index !== undefined) {
this.storeCurrentTabIndexInURL(index);
}
@ -171,6 +203,13 @@ export default {
this.currentTab = tab;
this.addTabToLoaded(tab);
},
shouldLoadTab(tab) {
const isLoaded = this.isTabLoaded(tab);
const isCurrent = this.isCurrent(tab);
const tabElLoaded = this.tabWidth !== undefined && this.tabHeight !== undefined;
return (isLoaded && isCurrent) || ((isLoaded && !isCurrent) && tabElLoaded);
},
showRemoveDialog(index) {
if (!this.tabsList[index]) {
return;
@ -325,6 +364,14 @@ export default {
this.currentTabIndex = tabIndex;
this.currentTab = this.tabsList[tabIndex];
},
handleWindowResize() {
if (!this.$refs.tabs || !this.$refs.tabsHolder) {
return;
}
this.tabWidth = this.$refs.tabs.offsetWidth + 'px';
this.tabHeight = this.$refs.tabsHolder.offsetHeight - this.$refs.tabs.offsetHeight + 'px';
}
}
};

View File

@ -20,7 +20,11 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
import {
createOpenMct,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
import TabsLayout from './plugin';
import Vue from "vue";
import EventEmitter from "EventEmitter";
@ -63,13 +67,13 @@ describe('the plugin', function () {
'phase': 5,
'randomness': 0
},
'name': 'Sine Wave Generator',
'type': 'generator',
'modified': 1592851063871,
'location': 'mine',
'persisted': 1592851063871
};
let telemetryItem1 = Object.assign({}, telemetryItemTemplate, {
'name': 'Sine Wave Generator 1',
'id': '55122607-e65e-44d5-9c9d-9c31a914ca89',
'identifier': {
'namespace': '',
@ -77,6 +81,7 @@ describe('the plugin', function () {
}
});
let telemetryItem2 = Object.assign({}, telemetryItemTemplate, {
'name': 'Sine Wave Generator 2',
'id': '55122607-e65e-44d5-9c9d-9c31a914ca90',
'identifier': {
'namespace': '',
@ -91,6 +96,9 @@ describe('the plugin', function () {
element = document.createElement('div');
child = document.createElement('div');
child.style.display = 'block';
child.style.width = '1920px';
child.style.height = '1080px';
element.appendChild(child);
openmct.on('start', done);
@ -150,8 +158,17 @@ describe('the plugin', function () {
let tabsLayoutViewProvider;
let mockComposition;
let count = 0;
let resizeCallback;
beforeEach(() => {
class mockResizeObserver {
constructor(cb) {
resizeCallback = cb;
}
observe() { }
disconnect() { }
}
mockComposition = new EventEmitter();
mockComposition.load = () => {
if (count === 0) {
@ -165,6 +182,9 @@ describe('the plugin', function () {
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
spyOnBuiltins(['ResizeObserver']);
window.ResizeObserver.and.callFake(mockResizeObserver);
const applicableViews = openmct.objectViews.get(testViewObject, []);
tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs');
let view = tabsLayoutViewProvider.view(testViewObject, []);
@ -175,6 +195,7 @@ describe('the plugin', function () {
afterEach(() => {
count = 0;
testViewObject.keep_alive = true;
});
it ('renders a tab for each item', () => {
@ -185,10 +206,22 @@ describe('the plugin', function () {
describe('with domainObject.keep_alive set to', () => {
it ('true, will keep all views loaded, regardless of current tab view', () => {
let tabViewEls = element.querySelectorAll('.c-tabs-view__object');
it ('true, will keep all views loaded, regardless of current tab view', (done) => {
resizeCallback();
expect(tabViewEls.length).toEqual(2);
// the function called by the resize observer is debounced 500ms,
// this is to account for that
let promise = new Promise((resolve, reject) => {
setTimeout(resolve, 501);
});
Promise.all([Vue.nextTick(), promise]).then(() => {
let tabViewEls = element.querySelectorAll('.c-tabs-view__object');
expect(tabViewEls.length).toEqual(2);
}).finally(() => {
done();
});
});
it ('false, will only keep the current tab view loaded', async () => {

View File

@ -113,7 +113,6 @@ import search from '../components/search.vue';
const ITEM_BUFFER = 25;
const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded';
const RETURN_ALL_DESCENDANTS = true;
const SORT_MY_ITEMS_ALPH_ASC = true;
const TREE_ITEM_INDENT_PX = 18;
@ -432,7 +431,7 @@ export default {
return scrollTopAmount >= treeStart && scrollTopAmount < treeEnd;
},
sortNameDescending(a, b) {
sortNameAscending(a, b) {
// sorting tree children items
if (!(a.name && b.name)) {
if (a.object.name.toLowerCase()
@ -459,15 +458,16 @@ export default {
return 0;
},
isSortable(parentObjectPath) {
// determine if any part of the parent's path includes a key value of mine; aka My Items
return Boolean(parentObjectPath.find(path => path.identifier.key === 'mine'));
},
async loadAndBuildTreeItemsFor(domainObject, parentObjectPath, abortSignal) {
let collection = this.openmct.composition.get(domainObject);
let composition = await collection.load(abortSignal);
// determine if any part of the parent's path includes a key value of mine; aka My Items
const isNestedInMyItems = Boolean(parentObjectPath.find(path => path.identifier.key === 'mine'));
if (SORT_MY_ITEMS_ALPH_ASC && isNestedInMyItems) {
const sortedComposition = composition.sort(this.sortNameDescending);
if (SORT_MY_ITEMS_ALPH_ASC && this.isSortable(parentObjectPath)) {
const sortedComposition = composition.slice().sort(this.sortNameAscending);
composition = sortedComposition;
}
@ -513,17 +513,35 @@ export default {
},
compositionAddHandler(navigationPath) {
return (domainObject) => {
let parentItem = this.getTreeItemByPath(navigationPath);
let newItem = this.buildTreeItem(domainObject, parentItem.objectPath, true);
let allDescendants = this.getChildrenInTreeFor(parentItem, RETURN_ALL_DESCENDANTS);
let afterItem = allDescendants.length ? allDescendants.pop() : parentItem;
const parentItem = this.getTreeItemByPath(navigationPath);
const newItem = this.buildTreeItem(domainObject, parentItem.objectPath, true);
const descendants = this.getChildrenInTreeFor(parentItem, true);
const directDescendants = this.getChildrenInTreeFor(parentItem);
this.addItemToTreeAfter(newItem, afterItem);
const isNestedInMyItems = Boolean(parentItem.objectPath && parentItem.objectPath.find(path => path.identifier.key === 'mine'));
if (directDescendants.length === 0) {
this.addItemToTreeAfter(newItem, parentItem);
if (SORT_MY_ITEMS_ALPH_ASC && isNestedInMyItems) {
this.sortTreeComposition(this.sortNameDescending, navigationPath);
return;
}
if (SORT_MY_ITEMS_ALPH_ASC && this.isSortable(parentItem.objectPath)) {
const newItemIndex = directDescendants
.findIndex(descendant => this.sortNameAscending(descendant, newItem) > 0);
const shouldInsertFirst = newItemIndex === 0;
const shouldInsertLast = newItemIndex === -1;
if (shouldInsertFirst) {
this.addItemToTreeAfter(newItem, parentItem);
} else if (shouldInsertLast) {
this.addItemToTreeAfter(newItem, descendants.pop());
} else {
this.addItemToTreeBefore(newItem, directDescendants[newItemIndex]);
}
return;
}
this.addItemToTreeAfter(newItem, descendants.pop());
};
},
compositionRemoveHandler(navigationPath) {
@ -555,17 +573,18 @@ export default {
const removeIndex = this.getTreeItemIndex(item.navigationPath);
this.treeItems.splice(removeIndex, 1);
},
sortTreeComposition(algorithem, parentPath) {
const parentIndex = this.getTreeItemIndex(parentPath);
const parentItem = this.treeItems[parentIndex];
addItemToTreeBefore(addItem, beforeItem) {
const addIndex = this.getTreeItemIndex(beforeItem.navigationPath);
const allDescendants = this.getChildrenInTreeFor(parentItem);
const sortedChildren = allDescendants.sort(algorithem);
this.treeItems.splice(parentIndex + 1, allDescendants.length, ...sortedChildren);
this.addItemToTree(addItem, addIndex);
},
addItemToTreeAfter(addItem, afterItem) {
const addIndex = this.getTreeItemIndex(afterItem.navigationPath);
this.treeItems.splice(addIndex + 1, 0, addItem);
this.addItemToTree(addItem, addIndex + 1);
},
addItemToTree(addItem, index) {
this.treeItems.splice(index, 0, addItem);
if (this.isTreeItemOpen(addItem)) {
this.openTreeItem(addItem);