Compare commits

...

9 Commits

Author SHA1 Message Date
Shefali Joshi
c756adad6f Move tests to their own describe block (#3447) 2020-10-09 14:29:52 -07:00
Andrew Henry
f3d593bc1e Cache gets (#3437)
* Cache gets

* Added test
2020-10-08 20:30:23 -07:00
Nikhil
b637307de6 [Notebook] Clicking new entry does not work as expected #3423 (#3434)
* [Notebook] Clicking new entry does not work as expected #3423

Co-authored-by: Joshi <simplyrender@gmail.com>
2020-10-08 16:56:37 -07:00
Shefali Joshi
b6e0208e71 Reverting when cancelling out of edits works for both legacy and new object providers (#3435)
* Update persistence capability to use object api get
* Getting objects using the legacy object service provider will use the defaultSpace if necessary
2020-10-08 16:48:26 -07:00
Shefali Joshi
631876cab3 Add missing APIs to legacy persistence adapter (#3433)
* Sends new style object to the object API for save when calling it from legacy persistence adapter

* Adds createObject and deleteObject methods to LegacyPersistenceAdapter
2020-10-08 10:45:06 -07:00
Shefali Joshi
a192d46c2b Sends new style object to the object API for save when calling it from legacy persistence adapter (#3431)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-10-07 16:44:20 -07:00
Jamie V
6923f17645 [Navigation Tree] Race condition on checking document readystate (#3430)
* checking if state is already ready, as this is a subcomponent, that could be the case

* optimizing readystate checks
2020-10-07 16:38:54 -07:00
Charles Hacskaylo
87a45de05b Fix scroll issues in tree overflow state (#3385)
* Fixes #3383 - Tree scrolling area should not display horizontal scroll.
* Includes various additional improvements to the object tree.
Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
2020-10-07 11:29:42 -07:00
Jamie V
ab76451360 Imagery Age to be displayed for realtime mode in Imagery View (#3308)
* fix linting errors

* removing testing units

* WIP: stubbe in age in template, adding getAge function

* WIP: stubbed in age in template, dummy function to start

* added image age for realtime mode, ready for styling

* reverting unnecesarry telemetryview file changes, not needed for this issue

* checking for age tracking conditions on mount

* Image age styling and changes

- Cleaned up code in ImageryPlugin to use const instead of var, changed
image delay time into a const

* Image age styling and changes

- WIP!
- Layout changes for Imagery control-bar;
- New animation effect, WIP;

* Image age styling and changes

- Markup and CSS updates for Imagery view;
- Final layout for age indicator;

* parsing image timestamp in case it is a string

* using moment for human readable durations above 8 hours

* UTC based timesystem check

* reset "new" css class on image age when "time" updates

* WIP: debuggin weird imagery plugin issue for first selection of image in thumbnails

* fixing pause overwriting clicked images selection

* making isImageNew a computed value

* WIP: pr updates

* WIP: tabling PR edits to focus on lower hanging PR edits for testathon

* WIP

* overhaul of imagery plugin logic for optimization PLUS imagery age

* adding next/prev functionality to refactored plugin

* added arrow left and right keys to navigate next and previous

* added arrow key scrolling and scrolling thumbnail into view and hold down scrolling

* adding in missing class

* component based key listening, PR updates

* refactor to use just imageIndex to track focused image, utilized more caching, PR comment edits

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-10-06 16:01:47 -07:00
17 changed files with 622 additions and 300 deletions

View File

@@ -27,7 +27,7 @@ define([
) {
function ImageryPlugin() {
var IMAGE_SAMPLES = [
const IMAGE_SAMPLES = [
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg",
@@ -47,13 +47,14 @@ define([
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
];
const IMAGE_DELAY = 20000;
function pointForTimestamp(timestamp, name) {
return {
name: name,
utc: Math.floor(timestamp / 5000) * 5000,
local: Math.floor(timestamp / 5000) * 5000,
url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length]
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]
};
}
@@ -64,7 +65,7 @@ define([
subscribe: function (domainObject, callback) {
var interval = setInterval(function () {
callback(pointForTimestamp(Date.now(), domainObject.name));
}, 5000);
}, IMAGE_DELAY);
return function () {
clearInterval(interval);
@@ -81,9 +82,9 @@ define([
var start = options.start;
var end = Math.min(options.end, Date.now());
var data = [];
while (start <= end && data.length < 5000) {
while (start <= end && data.length < IMAGE_DELAY) {
data.push(pointForTimestamp(start, domainObject.name));
start += 5000;
start += IMAGE_DELAY;
}
return Promise.resolve(data);

View File

@@ -153,10 +153,11 @@ define(["objectUtils"],
return this.$q.when(true);
}
return this.persistenceService.readObject(
this.getSpace(),
this.getKey()
).then(updateModel);
return this.openmct.objects.get(domainObject.getId()).then((newStyleObject) => {
let oldStyleObject = this.openmct.legacyObject(newStyleObject);
return updateModel(oldStyleObject.getModel());
});
};
/**

View File

@@ -37,6 +37,7 @@ define(
key = "persistence key",
id = "object identifier",
model,
refreshModel,
SPACE = "some space",
persistence,
mockOpenMCT,
@@ -60,6 +61,7 @@ define(
someKey: "some value",
name: "domain object"
};
refreshModel = {someOtherKey: "some other value"};
mockPersistenceService = jasmine.createSpyObj(
"persistenceService",
@@ -110,8 +112,16 @@ define(
}
});
mockOpenMCT = {};
mockOpenMCT.objects = jasmine.createSpyObj('Object API', ['save']);
mockOpenMCT = {
legacyObject: function (object) {
return {
getModel: function () {
return object;
}
};
}
};
mockOpenMCT.objects = jasmine.createSpyObj('Object API', ['save', 'get']);
mockIdentifierService.parse.and.returnValue(mockIdentifier);
mockIdentifier.getSpace.and.returnValue(SPACE);
@@ -131,6 +141,7 @@ define(
describe("successful persistence", function () {
beforeEach(function () {
mockOpenMCT.objects.save.and.returnValue(Promise.resolve(true));
mockOpenMCT.objects.get.and.returnValue(Promise.resolve(refreshModel));
});
it("creates unpersisted objects with the persistence service", function () {
// Verify precondition; no call made during constructor
@@ -146,11 +157,10 @@ define(
});
it("refreshes the domain object model from persistence", function () {
var refreshModel = {someOtherKey: "some other value"};
model.persisted = 1;
mockPersistenceService.readObject.and.returnValue(asPromise(refreshModel));
persistence.refresh();
expect(model).toEqual(refreshModel);
persistence.refresh().then(() => {
expect(model).toEqual(refreshModel);
});
});
it("does not trigger error notification on successful"

View File

@@ -128,7 +128,14 @@ define([
};
ObjectServiceProvider.prototype.get = function (key) {
const keyString = utils.makeKeyString(key);
let keyString = utils.makeKeyString(key);
const space = this.getSpace(keyString);
let identifier = utils.parseKeyString(keyString);
// We assign to the space for legacy persistence providers since they register themselves using a defaultSpace.
// This is the way to make everyone happy.
identifier.namespace = space;
keyString = utils.makeKeyString(identifier);
return this.objectService.getObjects([keyString])
.then(function (results) {

View File

@@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import utils from 'objectUtils';
export default class LegacyPersistenceAdapter {
constructor(openmct) {
this.openmct = openmct;
@@ -33,8 +35,31 @@ export default class LegacyPersistenceAdapter {
return Promise.resolve(Object.keys(this.openmct.objects.providers));
}
updateObject(legacyDomainObject) {
return this.openmct.objects.save(legacyDomainObject.useCapability('adapter'));
createObject(space, key, legacyDomainObject) {
let object = utils.toNewFormat(legacyDomainObject, {
namespace: space,
key: key
});
return this.openmct.objects.save(object);
}
deleteObject(space, key) {
const identifier = {
namespace: space,
key: key
};
return this.openmct.objects.delete(identifier);
}
updateObject(space, key, legacyDomainObject) {
let object = utils.toNewFormat(legacyDomainObject, {
namespace: space,
key: key
});
return this.openmct.objects.save(object);
}
readObject(space, key) {

View File

@@ -47,6 +47,7 @@ define([
this.providers = {};
this.rootRegistry = new RootRegistry();
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
this.cache = {};
}
/**
@@ -154,6 +155,11 @@ define([
* has been saved, or be rejected if it cannot be saved
*/
ObjectAPI.prototype.get = function (identifier) {
let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = utils.parseKeyString(identifier);
const provider = this.getProvider(identifier);
@@ -165,7 +171,15 @@ define([
throw new Error('Provider does not support get!');
}
return provider.get(identifier);
let objectPromise = provider.get(identifier);
this.cache[keystring] = objectPromise;
return objectPromise.then(result => {
delete this.cache[keystring];
return result;
});
};
ObjectAPI.prototype.delete = function () {

View File

@@ -59,4 +59,25 @@ describe("The Object API", () => {
});
});
});
describe("The get function", () => {
describe("when a provider is available", () => {
let mockProvider;
beforeEach(() => {
mockProvider = jasmine.createSpyObj("mock provider", [
"get"
]);
mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
});
it("Caches multiple requests for the same object", () => {
expect(mockProvider.get.calls.count()).toBe(0);
objectAPI.get(mockDomainObject.identifier);
expect(mockProvider.get.calls.count()).toBe(1);
objectAPI.get(mockDomainObject.identifier);
expect(mockProvider.get.calls.count()).toBe(1);
});
});
});
});

View File

@@ -1,5 +1,11 @@
<template>
<div class="c-imagery">
<div
tabindex="0"
class="c-imagery"
@keyup="arrowUpHandler"
@keydown="arrowDownHandler"
@mouseover="focusElement"
>
<div class="c-imagery__main-image-wrapper has-local-controls">
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc">
<span class="holder flex-elem grows c-imagery__lc__sliders">
@@ -22,98 +28,188 @@
></a>
</span>
</div>
<div class="main-image s-image-main c-imagery__main-image has-local-controls js-imageryView-image"
:class="{'paused unnsynced': paused(),'stale':false }"
:style="{'background-image': getImageUrl() ? `url(${getImageUrl()})` : 'none',
<div class="main-image s-image-main c-imagery__main-image has-local-controls"
:class="{'paused unnsynced': isPaused,'stale':false }"
:style="{'background-image': imageUrl ? `url(${imageUrl})` : 'none',
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
:data-openmct-image-timestamp="getTime()"
:data-openmct-object-keystring="keystring"
:data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString"
>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev"
title="Previous image"
:disabled="isPrevDisabled()"
:disabled="isPrevDisabled"
@click="prevImage()"
></button>
<button class="c-nav c-nav--next"
title="Next image"
:disabled="isNextDisabled()"
:disabled="isNextDisabled"
@click="nextImage()"
></button>
</div>
</div>
<div class="c-imagery__control-bar">
<div class="c-imagery__timestamp">{{ getTime() }}</div>
<div class="c-imagery__time">
<div class="c-imagery__timestamp">{{ time }}</div>
<div
v-if="canTrackDuration"
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
class="c-imagery__age icon-timer"
>{{ formattedDuration }}</div>
</div>
<div class="h-local-controls flex-elem">
<button
class="c-button icon-pause pause-play"
:class="{'is-paused': paused()}"
@click="paused(!paused(), true)"
:class="{'is-paused': isPaused}"
@click="paused(!isPaused, 'button')"
></button>
</div>
</div>
</div>
<div ref="thumbsWrapper"
class="c-imagery__thumbs-wrapper"
:class="{'is-paused': paused()}"
:class="{'is-paused': isPaused}"
@scroll="handleScroll"
>
<div v-for="(imageData, index) in imageHistory"
:key="index"
<div v-for="(datum, index) in imageHistory"
:key="datum.url"
class="c-imagery__thumb c-thumb"
:class="{selected: imageData.selected}"
@click="setSelectedImage(imageData)"
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)"
>
<img class="c-thumb__image"
:src="getImageUrl(imageData)"
:src="formatImageUrl(datum)"
>
<div class="c-thumb__timestamp">{{ getTime(imageData) }}</div>
<div class="c-thumb__timestamp">{{ formatTime(datum) }}</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
const DEFAULT_DURATION_FORMATTER = 'duration';
const REFRESH_CSS_MS = 500;
const DURATION_TRACK_MS = 1000;
const ARROW_DOWN_DELAY_CHECK_MS = 400;
const ARROW_SCROLL_RATE_MS = 100;
const THUMBNAIL_CLICKED = true;
const ONE_MINUTE = 60 * 1000;
const FIVE_MINUTES = 5 * ONE_MINUTE;
const ONE_HOUR = ONE_MINUTE * 60;
const EIGHT_HOURS = 8 * ONE_HOUR;
const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
const ARROW_RIGHT = 39;
const ARROW_LEFT = 37;
export default {
inject: ['openmct', 'domainObject'],
data() {
let timeSystem = this.openmct.time.timeSystem();
return {
autoScroll: true,
durationFormatter: undefined,
filters: {
brightness: 100,
contrast: 100
},
image: {
selected: ''
},
imageFormat: '',
imageHistory: [],
imageUrl: '',
thumbnailClick: THUMBNAIL_CLICKED,
isPaused: false,
metadata: {},
requestCount: 0,
timeFormat: '',
keystring: ''
timeSystem: timeSystem,
timeFormatter: undefined,
refreshCSS: false,
keyString: undefined,
focusedImageIndex: undefined,
numericDuration: undefined
};
},
computed: {
bounds() {
return this.openmct.time.bounds();
time() {
return this.formatTime(this.focusedImage);
},
imageUrl() {
return this.formatImageUrl(this.focusedImage);
},
isImageNew() {
let cutoff = FIVE_MINUTES;
let age = this.numericDuration;
return age < cutoff && !this.refreshCSS;
},
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
},
isNextDisabled() {
let disabled = false;
if (this.focusedImageIndex === -1 || this.focusedImageIndex === this.imageHistory.length - 1) {
disabled = true;
}
return disabled;
},
isPrevDisabled() {
let disabled = false;
if (this.focusedImageIndex === 0 || this.imageHistory.length < 2) {
disabled = true;
}
return disabled;
},
focusedImage() {
return this.imageHistory[this.focusedImageIndex];
},
parsedSelectedTime() {
return this.parseTime(this.focusedImage);
},
formattedDuration() {
let result = 'N/A';
let negativeAge = -1;
if (this.numericDuration > TWENTYFOUR_HOURS) {
negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS);
result = moment.duration(negativeAge, 'days').humanize(true);
} else if (this.numericDuration > EIGHT_HOURS) {
negativeAge *= (this.numericDuration / ONE_HOUR);
result = moment.duration(negativeAge, 'hours').humanize(true);
} else if (this.durationFormatter) {
result = this.durationFormatter.format(this.numericDuration);
}
return result;
}
},
watch: {
focusedImageIndex() {
this.trackDuration();
this.resetAgeCSS();
}
},
mounted() {
// set
this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageFormat = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
// initialize
this.timeKey = this.openmct.time.timeSystem().key;
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
// 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.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.requestHistory();
@@ -127,41 +223,55 @@ export default {
delete this.unsubscribe;
}
this.stopDurationTracking();
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
this.openmct.time.off('clock', this.clockChange);
},
methods: {
focusElement() {
this.$el.focus();
},
datumIsNotValid(datum) {
if (this.imageHistory.length === 0) {
return false;
}
const datumTime = this.timeFormat.format(datum);
const datumURL = this.imageFormat.format(datum);
const lastHistoryTime = this.timeFormat.format(this.imageHistory.slice(-1)[0]);
const lastHistoryURL = this.imageFormat.format(this.imageHistory.slice(-1)[0]);
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.timeFormat.parse(datum);
const historyTimeCheck = this.timeFormat.parse(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTime === lastHistoryTime) && (datumURL === lastHistoryURL);
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;
},
getImageUrl(datum) {
return datum
? this.imageFormat.format(datum)
: this.imageUrl;
formatImageUrl(datum) {
if (!datum) {
return;
}
return this.imageFormatter.format(datum);
},
getTime(datum) {
let dateTimeStr = datum
? this.timeFormat.format(datum)
: this.time;
formatTime(datum) {
if (!datum) {
return;
}
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr ? dateTimeStr.replace("T", " ") : "";
return dateTimeStr.replace("T", " ");
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
@@ -174,26 +284,35 @@ export default {
|| (scrollHeight - scrollTop) > 2 * clientHeight;
this.autoScroll = !disableScroll;
},
paused(state, button = false) {
if (arguments.length > 0 && state !== this.isPaused) {
this.unselectAllImages();
this.isPaused = state;
if (state === true && button) {
// If we are pausing, select the latest image in imageHistory
this.setSelectedImage(this.imageHistory[this.imageHistory.length - 1]);
}
paused(state, type) {
if (this.nextDatum) {
this.updateValues(this.nextDatum);
delete this.nextDatum;
} else {
this.updateValues(this.imageHistory[this.imageHistory.length - 1]);
}
this.isPaused = state;
this.autoScroll = true;
if (type === 'button') {
this.setFocusedImage(this.imageHistory.length - 1);
}
return this.isPaused;
if (this.nextImageIndex) {
this.setFocusedImage(this.nextImageIndex);
delete this.nextImageIndex;
}
this.autoScroll = true;
},
scrollToFocused() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper) {
return;
}
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
if (domThumb) {
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
},
scrollToRight() {
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
@@ -207,22 +326,17 @@ export default {
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
},
setSelectedImage(image) {
// If we are paused and the current image IS selected, unpause
// Otherwise, set current image and pause
if (!image) {
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
this.nextImageIndex = index;
return;
}
if (this.isPaused && image.selected) {
this.paused(false);
this.unselectAllImages();
} else {
this.imageUrl = this.getImageUrl(image);
this.time = this.getTime(image);
this.focusedImageIndex = index;
if (thumbnailClick && !this.isPaused) {
this.paused(true);
this.unselectAllImages();
image.selected = true;
}
},
boundsChange(bounds, isTick) {
@@ -230,98 +344,158 @@ export default {
this.requestHistory();
}
},
requestHistory() {
const requestId = ++this.requestCount;
async requestHistory() {
let bounds = this.openmct.time.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
this.openmct.telemetry
.request(this.domainObject, this.bounds)
.then((values = []) => {
if (this.requestCount === requestId) {
// add each image to the history
// update values for the very last image (set current image time and url)
values.forEach((datum, index) => this.updateHistory(datum, index === values.length - 1));
}
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) {
// reset timesystem dependent variables
this.timeKey = system.key;
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
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.timeFormat.parse(datum);
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
if (parsedTimestamp >= this.bounds.start && parsedTimestamp <= this.bounds.end) {
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
this.updateHistory(datum);
}
});
},
unselectAllImages() {
this.imageHistory.forEach(image => image.selected = false);
},
updateHistory(datum, updateValues = true) {
updateHistory(datum, setFocused = true) {
if (this.datumIsNotValid(datum)) {
return;
}
this.imageHistory.push(datum);
if (updateValues) {
this.updateValues(datum);
if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1);
}
},
updateValues(datum) {
if (this.isPaused) {
this.nextDatum = datum;
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();
this.updateDuration();
this.durationTracker = window.setInterval(
this.updateDuration, DURATION_TRACK_MS
);
} else {
this.stopDurationTracking();
}
},
stopDurationTracking() {
window.clearInterval(this.durationTracker);
},
updateDuration() {
let currentTime = this.openmct.time.clock().currentValue();
this.numericDuration = currentTime - this.parsedSelectedTime;
},
resetAgeCSS() {
this.refreshCSS = true;
// unable to make this work with nextTick
setTimeout(() => {
this.refreshCSS = false;
}, REFRESH_CSS_MS);
},
nextImage() {
if (this.isNextDisabled) {
return;
}
this.time = this.timeFormat.format(datum);
this.imageUrl = this.imageFormat.format(datum);
},
selectedImageIndex() {
return this.imageHistory.findIndex(image => image.selected);
},
setSelectedByIndex(index) {
this.setSelectedImage(this.imageHistory[index]);
},
nextImage() {
let index = this.selectedImageIndex();
this.setSelectedByIndex(++index);
let index = this.focusedImageIndex;
this.setFocusedImage(++index, THUMBNAIL_CLICKED);
if (index === this.imageHistory.length - 1) {
this.paused(false);
}
},
prevImage() {
let index = this.selectedImageIndex();
if (index === -1) {
this.setSelectedByIndex(this.imageHistory.length - 2);
if (this.isPrevDisabled) {
return;
}
let index = this.focusedImageIndex;
if (index === this.imageHistory.length - 1) {
this.setFocusedImage(this.imageHistory.length - 2, THUMBNAIL_CLICKED);
} else {
this.setSelectedByIndex(--index);
this.setFocusedImage(--index, THUMBNAIL_CLICKED);
}
},
isNextDisabled() {
let disabled = false;
let index = this.selectedImageIndex();
arrowDownHandler(event) {
let key = event.keyCode;
if (index === -1 || index === this.imageHistory.length - 1) {
disabled = true;
if (this.isLeftOrRightArrowKey(key)) {
this.arrowDown = true;
window.clearTimeout(this.arrowDownDelayTimeout);
this.arrowDownDelayTimeout = window.setTimeout(() => {
this.arrowKeyScroll(this.directionByKey(key));
}, ARROW_DOWN_DELAY_CHECK_MS);
}
return disabled;
},
isPrevDisabled() {
let disabled = false;
let index = this.selectedImageIndex();
arrowUpHandler(event) {
let key = event.keyCode;
if (index === 0 || this.imageHistory.length < 2) {
disabled = true;
window.clearTimeout(this.arrowDownDelayTimeout);
if (this.isLeftOrRightArrowKey(key)) {
this.arrowDown = false;
let direction = this.directionByKey(key);
this[direction + 'Image']();
}
},
arrowKeyScroll(direction) {
if (this.arrowDown) {
this.arrowKeyScrolling = true;
this[direction + 'Image']();
setTimeout(() => {
this.arrowKeyScroll(direction);
}, ARROW_SCROLL_RATE_MS);
} else {
window.clearTimeout(this.arrowDownDelayTimeout);
this.arrowKeyScrolling = false;
this.scrollToFocused();
}
},
directionByKey(keyCode) {
let direction;
if (keyCode === ARROW_LEFT) {
direction = 'prev';
}
return disabled;
if (keyCode === ARROW_RIGHT) {
direction = 'next';
}
return direction;
},
isLeftOrRightArrowKey(keyCode) {
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
}
}
};

View File

@@ -4,6 +4,10 @@
overflow: hidden;
height: 100%;
&:focus {
outline: none;
}
> * + * {
margin-top: $interiorMargin;
}
@@ -25,14 +29,57 @@
}
}
&__control-bar {
padding: 5px 0 0 0;
&__control-bar,
&__time {
display: flex;
align-items: center;
align-items: baseline;
> * + * {
margin-left: $interiorMarginSm;
}
}
&__control-bar {
margin-top: 2px;
padding: $interiorMarginSm 0;
justify-content: space-between;
}
&__time {
flex: 0 1 auto;
overflow: hidden;
}
&__timestamp,
&__age {
@include ellipsize();
flex: 0 1 auto;
}
&__timestamp {
flex: 1 1 auto;
flex-shrink: 10;
}
&__age {
border-radius: $controlCr;
display: flex;
flex-shrink: 0;
align-items: baseline;
padding: 1px $interiorMarginSm;
&:before {
opacity: 0.5;
margin-right: $interiorMarginSm;
}
}
&--new {
// New imagery
$bgColor: $colorOk;
background: rgba($bgColor, 0.5);
@include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
}
&__thumbs-wrapper {
@@ -151,6 +198,8 @@
/*************************************** BUTTONS */
.c-button.pause-play {
// Pause icon set by default in markup
justify-self: end;
&.is-paused {
background: $colorPausedBg !important;
color: $colorPausedFg;

View File

@@ -111,11 +111,9 @@ import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
import { DEFAULT_CLASS, addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
import { throttle } from 'lodash';
const DEFAULT_CLASS = 'is-notebook-default';
export default {
inject: ['openmct', 'domainObject', 'snapshotContainer'],
components: {
@@ -197,15 +195,6 @@ export default {
});
},
methods: {
addDefaultClass() {
const classList = this.internalDomainObject.classList || [];
if (classList.includes(DEFAULT_CLASS)) {
return;
}
classList.push(DEFAULT_CLASS);
this.mutateObject('classList', classList);
},
changeSelectedSection({ sectionId, pageId }) {
const sections = this.sections.map(s => {
s.isSelected = false;
@@ -442,11 +431,18 @@ export default {
},
async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.openmct, notebookStorage);
this.addDefaultClass();
this.defaultSectionId = notebookStorage.section.id;
this.defaultPageId = notebookStorage.page.id;
if (defaultNotebookObject.identifier.key !== notebookStorage.notebookMeta.identifier.key) {
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.openmct, notebookStorage);
}
if (this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
this.defaultSectionId = notebookStorage.section.id;
}
if (this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
this.defaultPageId = notebookStorage.page.id;
}
},
updateDefaultNotebookPage(pages, id) {
if (!id) {

View File

@@ -1,5 +1,6 @@
import objectLink from '../../../ui/mixins/object-link';
export const DEFAULT_CLASS = 'is-notebook-default';
const TIME_BOUNDS = {
START_BOUND: 'tc.startBound',
END_BOUND: 'tc.endBound',
@@ -128,6 +129,7 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
embeds
});
addDefaultClass(domainObject);
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
return id;
@@ -193,5 +195,15 @@ export function deleteNotebookEntries(openmct, domainObject, selectedSection, se
}
delete entries[selectedSection.id][selectedPage.id];
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
}
function addDefaultClass(domainObject) {
const classList = domainObject.classList || [];
if (classList.includes(DEFAULT_CLASS)) {
return;
}
classList.push(DEFAULT_CLASS);
}

View File

@@ -60,7 +60,6 @@ export function setDefaultNotebookSection(section) {
notebookStorage.section = section;
saveDefaultNotebook(notebookStorage);
}
export function setDefaultNotebookPage(page) {

View File

@@ -22,9 +22,10 @@
import CouchObjectProvider from './CouchObjectProvider';
const NAMESPACE = '';
const PERSISTENCE_SPACE = '';
export default function CouchPlugin(url) {
return function install(openmct) {
openmct.objects.addProvider(NAMESPACE, new CouchObjectProvider(openmct, url, NAMESPACE));
openmct.objects.addProvider(PERSISTENCE_SPACE, new CouchObjectProvider(openmct, url, NAMESPACE));
};
}

View File

@@ -50,6 +50,18 @@
}
/************************** EFFECTS */
@mixin flash($animName: flash, $dur: 500ms, $dir: alternate, $iter: 20, $prop: background, $valStart: rgba($colorOk, 1), $valEnd: rgba($colorOk, 0)) {
@keyframes #{$animName} {
0% { #{$prop}: $valStart; }
100% { #{$prop}: $valEnd; }
}
animation-name: $animName;
animation-duration: $dur;
animation-direction: $dir;
animation-iteration-count: $iter;
animation-timing-function: ease-out;
}
@mixin mixedBg() {
$c1: nth($mixedSettingBg, 1);
$c2: nth($mixedSettingBg, 2);

View File

@@ -26,10 +26,8 @@
overflow: hidden;
transition: all;
.scrollable-children {
.c-tree__item-h {
width: 100%;
}
&__scrollable-children {
overflow: auto;
}
&__item--empty {
@@ -57,7 +55,11 @@
li {
position: relative;
&[class*="__item-h"] { display: block; }
&[class*="__item-h"] {
display: block;
width: 100%;
}
+ li {
margin-top: 1px;
}

View File

@@ -35,7 +35,7 @@
class="c-tree-and-search__tree c-tree"
>
<!-- ancestors -->
<div v-if="!activeSearch">
<li v-if="!activeSearch">
<tree-item
v-for="(ancestor, index) in ancestors"
:key="ancestor.id"
@@ -43,20 +43,20 @@
:show-up="index < ancestors.length - 1"
:show-down="false"
:left-offset="index * 10 + 'px'"
:emit-height="getChildHeight"
@emittedHeight="setChildHeight"
:should-emit-height="shouldEmitHeight"
@emittedHeight="setItemHeight"
@resetTree="handleReset"
/>
<!-- loading -->
<li
v-if="isLoading"
<div
v-if="isLoading || !itemHeightCalculated"
:style="indicatorLeftOffset"
class="c-tree__item c-tree-and-search__loading loading"
>
<span class="c-tree__item__label">Loading...</span>
</li>
</div>
<!-- end loading -->
</div>
</li>
<!-- currently viewed children -->
<transition
@@ -64,17 +64,17 @@
appear
>
<li
v-if="!isLoading && !searchLoading"
v-if="!isLoading && !searchLoading && itemHeightCalculated"
:style="childrenListStyles()"
:class="childrenSlideClass"
>
<ul
ref="scrollable"
class="scrollable-children"
class="c-tree__scrollable-children"
:style="scrollableStyles()"
@scroll="scrollItems"
>
<div :style="{ height: childrenHeight + 'px'}">
<div :style="{ height: childrenHeight + 'px' }">
<tree-item
v-for="(treeItem, index) in visibleItems"
:key="treeItem.id"
@@ -83,7 +83,7 @@
:item-offset="itemOffset"
:item-index="index"
:item-height="itemHeight"
:virtual-scroll="!noScroll"
:virtual-scroll="true"
:show-down="activeSearch ? false : true"
@expanded="handleExpanded"
/>
@@ -143,20 +143,18 @@ export default {
ancestors: [],
childrenSlideClass: 'down',
availableContainerHeight: 0,
noScroll: true,
updatingView: false,
itemHeightCalculated: false,
itemHeight: 28,
itemOffset: 0,
childrenHeight: 0,
scrollable: undefined,
pageThreshold: 50,
activeSearch: false,
getChildHeight: false,
settingChildrenHeight: false,
shouldEmitHeight: false,
isMobile: isMobile.mobileName,
multipleRootChildren: false,
noVisibleItems: false,
observedAncestors: {}
observedAncestors: {},
mainTreeTopMargin: undefined
};
},
computed: {
@@ -189,6 +187,21 @@ export default {
return {
paddingLeft: offset + 'px'
};
},
ancestorsHeight() {
if (this.activeSearch) {
return 0;
}
return this.itemHeight * this.ancestors.length;
},
pageThreshold() {
return Math.ceil(this.availableContainerHeight / this.itemHeight) + ITEM_BUFFER;
},
childrenHeight() {
let childrenCount = this.focusedItems.length || 1;
return (this.itemHeight * childrenCount) - this.mainTreeTopMargin; // 5px margin
}
},
watch: {
@@ -199,7 +212,7 @@ export default {
let jumpAndScroll = currentLocationPath
&& hasParent
&& !this.currentPathIsActivePath();
let justScroll = this.currentPathIsActivePath() && !this.noScroll;
let justScroll = this.currentPathIsActivePath();
if (this.searchValue) {
this.searchValue = '';
@@ -233,20 +246,27 @@ export default {
this.searchDeactivated();
}
},
searchResultItems() {
this.setContainerHeight();
},
allTreeItems() {
// catches an edge case race condition and when new items are added (ex. folder)
// catches an edge case when new items are added (ex. folder)
if (!this.isLoading) {
this.setContainerHeight();
}
},
ancestors() {
this.observeAncestors();
},
availableContainerHeight() {
this.updateVisibleItems();
},
focusedItems() {
this.updateVisibleItems();
}
},
async mounted() {
// only reliable way to get final tree top margin
this.readyStateCheck();
this.backwardsCompatibilityCheck();
let savedPath = this.getSavedNavigatedPath();
@@ -257,13 +277,20 @@ export default {
if (root.identifier !== undefined) {
let rootNode = this.buildTreeItem(root);
let rootCompositionCollection = this.openmct.composition.get(root);
let rootComposition = await rootCompositionCollection.load();
// if more than one root item, set multipleRootChildren to true and add root to ancestors
if (root.composition && root.composition.length > 1) {
if (rootComposition && rootComposition.length > 1) {
this.ancestors.push(rootNode);
if (!this.itemHeightCalculated) {
await this.calculateItemHeight();
}
this.multipleRootChildren = true;
} else if (!savedPath && root.composition[0] !== undefined) {
} else if (!savedPath && rootComposition[0] !== undefined) {
// needed if saved path is not set, need to set it to the only root child
savedPath = root.composition[0];
savedPath = rootComposition[0].identifier;
}
if (savedPath) {
@@ -282,13 +309,32 @@ export default {
},
created() {
this.getSearchResults = _.debounce(this.getSearchResults, 400);
this.setContainerHeight = _.debounce(this.setContainerHeight, 200);
},
updated() {
this.$nextTick(() => {
this.setContainerHeight();
});
},
destroyed() {
window.removeEventListener('resize', this.handleWindowResize);
this.stopObservingAncestors();
document.removeEventListener('readystatechange', this.setTreeTopMargin);
},
methods: {
updatevisibleItems() {
readyStateCheck() {
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.setTreeTopMargin);
} else {
this.setTreeTopMargin();
}
},
setTreeTopMargin() {
if (document.readyState === 'complete') {
this.mainTreeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop');
}
},
updateVisibleItems() {
if (this.updatingView) {
return;
}
@@ -326,105 +372,54 @@ export default {
this.updatingView = false;
});
},
async setContainerHeight() {
await this.$nextTick();
setContainerHeight() {
let mainTree = this.$refs.mainTree;
let mainTreeHeight = mainTree && mainTree.clientHeight ? mainTree.clientHeight : 0;
if (mainTreeHeight !== 0) {
this.calculateChildHeight(() => {
let ancestorsHeight = this.calculateAncestorHeight();
let allChildrenHeight = this.calculateChildrenHeight();
if (this.activeSearch) {
ancestorsHeight = 0;
}
this.availableContainerHeight = mainTreeHeight - ancestorsHeight;
if (allChildrenHeight > this.availableContainerHeight) {
this.setPageThreshold();
this.noScroll = false;
} else {
this.noScroll = true;
}
this.updatevisibleItems();
});
this.availableContainerHeight = mainTreeHeight - this.ancestorsHeight;
} else {
window.setTimeout(this.setContainerHeight, RECHECK_DELAY);
}
},
calculateFirstVisibleItem() {
if (!this.$refs.scrollable) {
return;
}
let scrollTop = this.$refs.scrollable.scrollTop;
return Math.floor(scrollTop / this.itemHeight);
},
calculateLastVisibleItem() {
if (!this.$refs.scrollable) {
return;
}
let scrollBottom = this.$refs.scrollable.scrollTop + this.$refs.scrollable.offsetHeight;
return Math.ceil(scrollBottom / this.itemHeight);
},
calculateChildrenHeight() {
let mainTreeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop');
let childrenCount = this.focusedItems.length;
calculateItemHeight() {
this.shouldEmitHeight = true;
return (this.itemHeight * childrenCount) - mainTreeTopMargin; // 5px margin
return new Promise((resolve, reject) => {
this.itemHeightResolve = resolve;
});
},
setChildrenHeight() {
this.childrenHeight = this.calculateChildrenHeight();
},
calculateAncestorHeight() {
let ancestorCount = this.ancestors.length;
async setItemHeight(height) {
return this.itemHeight * ancestorCount;
},
calculateChildHeight(callback) {
if (callback) {
this.afterChildHeight = callback;
}
if (!this.activeSearch) {
this.getChildHeight = true;
} else if (this.afterChildHeight) {
// keep the height from before
this.afterChildHeight();
delete this.afterChildHeight;
}
},
async setChildHeight(item) {
if (!this.getChildHeight || this.settingChildrenHeight) {
if (this.itemHeightCalculated) {
return;
}
this.settingChildrenHeight = true;
if (this.isMobile) {
item = item.children[0];
}
await this.$nextTick();
let topMargin = this.getElementStyleValue(item, 'marginTop');
let bottomMargin = this.getElementStyleValue(item, 'marginBottom');
let totalVerticalMargin = topMargin + bottomMargin;
this.itemHeight = item.clientHeight + totalVerticalMargin;
this.setChildrenHeight();
if (this.afterChildHeight) {
this.afterChildHeight();
delete this.afterChildHeight;
}
this.itemHeight = height;
this.itemHeightCalculated = true;
this.shouldEmitHeight = false;
this.getChildHeight = false;
this.settingChildrenHeight = false;
},
setPageThreshold() {
let threshold = Math.ceil(this.availableContainerHeight / this.itemHeight) + ITEM_BUFFER;
// all items haven't loaded yet (nextTick not working for this)
if (threshold === ITEM_BUFFER) {
window.setTimeout(this.setPageThreshold, RECHECK_DELAY);
} else {
this.pageThreshold = threshold;
}
this.itemHeightResolve();
},
handleWindowResize() {
if (!windowResizing) {
@@ -544,6 +539,9 @@ export default {
let currentNode = await this.openmct.objects.get(nodes[i]);
let newParent = this.buildTreeItem(currentNode);
this.ancestors.push(newParent);
if (!this.itemHeightCalculated) {
await this.calculateItemHeight();
}
if (i === nodes.length - 1) {
this.jumpPath = '';
@@ -570,14 +568,8 @@ export default {
let scrollTopAmount = indexOfScroll * this.itemHeight;
await this.$nextTick();
this.$refs.scrollable.scrollTop = scrollTopAmount;
// race condition check
if (scrollTopAmount > 0 && this.$refs.scrollable.scrollTop === 0) {
window.setTimeout(this.autoScroll, RECHECK_DELAY);
return;
}
this.scrollTo = undefined;
} else {
window.setTimeout(this.autoScroll, RECHECK_DELAY);
@@ -635,7 +627,6 @@ export default {
this.activeSearch = false;
await this.$nextTick();
this.$refs.scrollable.scrollTop = 0;
this.setContainerHeight();
},
handleReset(node) {
this.childrenSlideClass = 'up';
@@ -688,17 +679,16 @@ export default {
},
scrollItems(event) {
if (!windowResizing) {
this.updatevisibleItems();
this.updateVisibleItems();
}
},
childrenListStyles() {
return { position: 'relative' };
},
scrollableStyles() {
return {
height: this.availableContainerHeight + 'px',
overflow: this.noScroll ? 'hidden' : 'scroll'
};
let height = this.availableContainerHeight + 'px';
return { height };
},
getElementStyleValue(el, style) {
let styleString = window.getComputedStyle(el)[style];

View File

@@ -84,7 +84,7 @@ export default {
type: Boolean,
default: false
},
emitHeight: {
shouldEmitHeight: {
type: Boolean,
default: false
}
@@ -116,16 +116,14 @@ export default {
watch: {
expanded() {
this.$emit('expanded', this.domainObject);
},
emitHeight() {
this.$nextTick(() => {
this.$emit('emittedHeight', this.$refs.me);
});
}
},
mounted() {
let objectComposition = this.openmct.composition.get(this.node.object);
// only reliable way to get final item height
this.readyStateCheck();
this.domainObject = this.node.object;
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
this.domainObject = newObject;
@@ -137,14 +135,24 @@ export default {
}
this.openmct.router.on('change:path', this.highlightIfNavigated);
if (this.emitHeight) {
this.$emit('emittedHeight', this.$refs.me);
}
},
destroyed() {
this.openmct.router.off('change:path', this.highlightIfNavigated);
document.removeEventListener('readystatechange', this.emitHeight);
},
methods: {
readyStateCheck() {
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.emitHeight);
} else {
this.emitHeight();
}
},
emitHeight() {
if (this.shouldEmitHeight && document.readyState === 'complete') {
this.$emit('emittedHeight', this.$el.offsetHeight);
}
},
buildPathString(parentPath) {
return [parentPath, this.openmct.objects.makeKeyString(this.node.object.identifier)].join('/');
},