Compare commits

...

13 Commits

Author SHA1 Message Date
b2dadaeb45 new tests for imagery plugin 2020-12-16 13:12:37 -08:00
a98cf17b58 Merge branch 'master' into imagery-tests
Merg'n master
2020-12-15 10:16:54 -08:00
a874e906a0 Merge branch 'master' into imagery-tests
Merg'n master
2020-12-14 12:03:46 -08:00
d1f1707893 removeing unneccesary assignment for click.stop 2020-12-11 15:03:31 -08:00
b7ec3605a7 stop propagation of number input click 2020-12-11 14:59:12 -08:00
8d7d7d8211 close on click outside, enter to submit 2020-12-11 14:48:10 -08:00
9697c20dc1 Merge branch 'master' into proto-new-tc-input 2020-12-11 13:55:56 -08:00
26266ab831 Merge branch 'master' into proto-new-tc-input 2020-09-29 15:13:22 -07:00
949f45b31c Merge branch 'master' into proto-new-tc-input 2020-09-02 17:26:43 -07:00
690c0e7466 hooked up functionality of new time inputs 2020-08-21 16:23:47 -07:00
cce6fe0f31 Prototype mockup of new Time Conductor input component
- Refined font-sizes;
2020-08-05 19:01:50 -07:00
4c5a3362a0 Prototype mockup of new Time Conductor input component
- Added mouse wheel incrementing with padding, stepping, rounding and
min/max handling;
2020-08-05 18:59:02 -07:00
fc87b3afec Prototype mockup of new Time Conductor input component
- Added 'timePopup.vue' component with functional interactions and
styling;
- Added new component to Conductor.vue;
2020-08-05 17:48:21 -07:00
4 changed files with 506 additions and 4 deletions

View File

@ -0,0 +1,258 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageryPlugin from './plugin.js';
import Vue from 'vue';
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
const ONE_MINUTE = 1000 * 60;
const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500;
function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
let timestamp = imageElement.dataset.openmctImageTimestamp;
let identifier = imageElement.dataset.openmctObjectKeystring;
let url = imageElement.style.backgroundImage;
return {
timestamp,
identifier,
url
};
}
function isNew(doc) {
let newIcon = doc.querySelectorAll(NEW_IMAGE_CLASS);
return newIcon.length !== 0;
}
function generateTelemetry(start, count) {
let telemetry = [];
for (let i = 1, l = count + 1; i < l; i++) {
let stringRep = i + 'minute';
let logo = 'images/logo-openmct.svg';
telemetry.push({
"name": stringRep + " Imagery",
"utc": start + (i * ONE_MINUTE),
"url": location.host + '/' + logo + '?time=' + stringRep,
"timeId": stringRep
});
}
return telemetry;
}
describe("The Imagery View Layout", () => {
const imageryKey = 'example.imagery';
const START = Date.now();
const COUNT = 10;
let openmct;
let imageryPlugin;
let parent;
let child;
let timeFormat = 'utc';
let bounds = {
start: START - TEN_MINUTES,
end: START
};
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
let imageryObject = {
identifier: {
namespace: "",
key: "imageryId"
},
name: "Example Imagery",
type: "example.imagery",
location: "parentId",
modified: 0,
persisted: 0,
telemetry: {
values: [
{
"name": "Image",
"key": "url",
"format": "image",
"hints": {
"image": 1,
"priority": 3
},
"source": "url"
},
{
"name": "Name",
"key": "name",
"source": "name",
"hints": {
"priority": 0
}
},
{
"name": "Time",
"key": "utc",
"format": "utc",
"hints": {
"domain": 2,
"priority": 1
},
"source": "utc"
},
{
"name": "Local Time",
"key": "local",
"format": "local-format",
"hints": {
"domain": 1,
"priority": 2
},
"source": "local"
}
]
}
};
// this setups up the app
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
parent = document.createElement('div');
child = document.createElement('div');
parent.appendChild(child);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
imageryPlugin = new ImageryPlugin();
openmct.install(imageryPlugin);
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
openmct.time.timeSystem(timeFormat, {
start: 0,
end: 4
});
openmct.on('start', done);
openmct.startHeadless(appHolder);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it("should provide an imagery view only for imagery producing objects", () => {
let applicableViews = openmct.objectViews.get(imageryObject);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
expect(imageryView).toBeDefined();
});
describe("imagery view", () => {
let applicableViews;
let imageryViewProvider;
let imageryView;
beforeEach(async (done) => {
let telemetryRequestResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(imageTelemetry);
return telemetryRequestPromise;
});
openmct.time.clock('local', {
start: bounds.start,
end: bounds.end + 100
});
applicableViews = openmct.objectViews.get(imageryObject);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject);
imageryView.show(child);
await telemetryRequestPromise;
await Vue.nextTick();
return done();
});
it("on mount should show the the most recent image", () => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
it("should show the clicked thumbnail as the main image", async () => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
});
it("should show that an image is new", async (done) => {
await Vue.nextTick();
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeTrue();
done();
}, REFRESH_CSS_MS);
});
it("should show that an image is not new", async (done) => {
const target = imageTelemetry[2].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeFalse();
done();
}, REFRESH_CSS_MS);
});
});
});

View File

@ -73,6 +73,15 @@
>
<!-- RT start -->
<div class="c-direction-indicator icon-minus"></div>
<time-popup
v-if="showTCInputStart"
class="pr-tc-input-menu--start"
:type="'start'"
:offset="offsets.start"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<input
ref="startOffset"
v-model="offsets.start"
@ -81,6 +90,7 @@
autocorrect="off"
spellcheck="false"
@change="validateAllOffsets(); submitForm()"
@click="showTimePopupStart"
>
</div>
@ -97,7 +107,7 @@
autocorrect="off"
spellcheck="false"
:disabled="!isFixed"
@change="validateAllBounds('endDate'); submitForm()"
@change="validateAllBounds('endDate'); submitForm ()"
>
<date-picker
v-if="isFixed && isUTCBased"
@ -114,6 +124,15 @@
>
<!-- RT end -->
<div class="c-direction-indicator icon-plus"></div>
<time-popup
v-if="showTCInputEnd"
class="pr-tc-input-menu--end"
:type="'end'"
:offset="offsets.end"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<input
ref="endOffset"
v-model="offsets.end"
@ -122,6 +141,7 @@
autocorrect="off"
spellcheck="false"
@change="validateAllOffsets(); submitForm()"
@click="showTimePopupEnd"
>
</div>
@ -163,6 +183,7 @@ import DatePicker from './DatePicker.vue';
import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorHistory from './ConductorHistory.vue';
import TimePopup from './timePopup.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
@ -174,7 +195,8 @@ export default {
DatePicker,
ConductorAxis,
ConductorModeIcon,
ConductorHistory
ConductorHistory,
TimePopup
},
data() {
let bounds = this.openmct.time.bounds();
@ -208,7 +230,9 @@ export default {
showDatePicker: false,
altPressed: false,
isPanning: false,
isZooming: false
isZooming: false,
showTCInputStart: false,
showTCInputEnd: false
};
},
computed: {
@ -457,6 +481,25 @@ export default {
this.formattedBounds.end = this.timeFormatter.format(date);
this.validateAllBounds('endDate');
this.submitForm();
},
hideAllTimePopups() {
this.showTCInputStart = false;
this.showTCInputEnd = false;
},
showTimePopupStart() {
this.hideAllTimePopups();
this.showTCInputStart = !this.showTCInputStart;
},
showTimePopupEnd() {
this.hideAllTimePopups();
this.showTCInputEnd = !this.showTCInputEnd;
},
timePopUpdate(opts) {
let { type, hours, minutes, seconds } = opts;
this.offsets[type] = [hours, minutes, seconds].join(':');
this.setOffsetsFromView();
this.hideAllTimePopups();
}
}
};

View File

@ -207,7 +207,7 @@
}
.is-realtime-mode {
button {
.c-conductor__controls button {
@include themedButton($colorTimeBg);
color: $colorTimeFg;
@ -236,3 +236,77 @@
}
}
}
// Prototype
[class^='pr-tc-input-menu'] {
background: $colorBodyBg;
border-radius: $controlCr;
filter: brightness(1.4);
box-shadow: $shdwMenu;
padding: $interiorMargin;
position: absolute;
width: 170px;
height: 90px;
bottom: 20px;
z-index: 99;
&[class*='--start'] {
left: -25px;
}
&[class*='--end'] {
right: 0;
}
> * + * {
margin-top: $interiorMargin;
}
}
[class^='pr-tim'] {
display: flex;
align-items: center;
justify-content: flex-start;
> * + * {
//margin-left: $interiorMarginSm;
}
&[class*='labels'] {
font-size: 0.8em;
[class*='__'] {
opacity: 0.6;
width: 50px;
}
[class*='_hrs'] {
width: 67px;
}
}
&[class*='inputs'] {
//background: deeppink;
border: 1px solid rgba($colorBodyFg, 0.2);
border-radius: $controlCr;
padding: 2px;
input {
font-size: 1.25em;
width: 42px;
}
[class*='_hrs'] {
width: 52px;
}
[class*="colon"] {
padding: 0 2px;
}
}
&[class*='__buttons'] {
justify-content: flex-end;
}
}

View File

@ -0,0 +1,127 @@
<template>
<div
class="pr-tc-input-menu"
@keydown.enter.prevent
@keyup.enter.prevent="submit"
@click.stop
>
<div class="pr-tim-labels">
<div class="pr-time-label__hrs">Hrs</div>
<div class="pr-time-label__mins">Mins</div>
<div class="pr-time-label__secs">Secs</div>
</div>
<div class="pr-tim-inputs">
<input
ref="inputHrs"
v-model="inputHrs"
class="pr-time-input__hrs"
step="1"
type="number"
min="0"
max="999"
@focusin="selectAll($event)"
@focusout="format('inputHrs')"
@wheel="increment($event, 'inputHrs')"
>
<span class="pr-tim-colon">:</span>
<input
ref="inputMins"
v-model="inputMins"
type="number"
class="pr-time-input__mins"
min="0"
max="59"
step="1"
@focusin="selectAll($event)"
@focusout="format('inputMins')"
@wheel="increment($event, 'inputMins')"
>
<span class="pr-tim-colon">:</span>
<input
ref="inputSecs"
v-model="inputSecs"
type="number"
class="pr-time-input__secs"
min="0"
max="59"
step="1"
@focusin="selectAll($event)"
@focusout="format('inputSecs')"
@wheel="increment($event, 'inputSecs')"
>
</div>
<div class="pr-tim__buttons c-button-set c-button-set--strip-h">
<button class="c-button icon-check"
@click.prevent="submit"
></button>
<button class="c-button icon-x"
@click.prevent="hide"
></button>
</div>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
required: true
},
offset: {
type: String,
required: true
}
},
data() {
return {
inputHrs: '000',
inputMins: '00',
inputSecs: '00'
};
},
mounted() {
this.setOffset();
document.addEventListener('click', this.hide);
},
beforeDestroy() {
document.removeEventListener('click', this.hide);
},
methods: {
format(ref) {
const curVal = this[ref];
const padAmt = (ref === 'inputHrs') ? 3 : 2;
this[ref] = curVal.padStart(padAmt, '0');
},
submit() {
this.$emit('update', {
type: this.type,
hours: this.inputHrs,
minutes: this.inputMins,
seconds: this.inputSecs
});
},
hide() {
this.$emit('hide');
},
increment($ev, ref) {
$ev.preventDefault();
const padAmt = (ref === 'inputHrs') ? 3 : 2;
const step = (ref === 'inputHrs') ? 1 : 5;
const maxVal = (ref === 'inputHrs') ? 999 : 59;
let cv = Math.round(parseInt(this[ref], 10) / step) * step;
cv = Math.min(maxVal, Math.max(0, ($ev.deltaY < 0) ? cv + step : cv - step));
this[ref] = cv.toString().padStart(padAmt, '0');
},
setOffset() {
[this.inputHrs, this.inputMins, this.inputSecs] = this.offset.split(':');
this.inputHrs = this.inputHrs.padStart(3, '0');
this.$refs.inputHrs.focus();
},
selectAll($ev) {
$ev.target.select();
}
}
};
</script>