Timelist bug fixes (#6661)

* Ensure timelist scrolling happens correctly for clock as well as fixed time

* If an activity has already started, show the duration as time to/since the end of the activity

* Addresses review comments: Reverse +/- indicators, removes milliseconds from times.

* Fix linting issues

* Add e2e test for timelist display

* Scroll to 'now' if available
This commit is contained in:
Shefali Joshi 2023-06-16 12:45:59 -07:00 committed by GitHub
parent 4c5de37cff
commit 022dffd419
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 184 additions and 22 deletions

View File

@ -0,0 +1,122 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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 { test, expect } = require('../../../pluginFixtures');
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions');
const testPlan = {
TEST_GROUP: [
{
name: 'Past event 1',
start: 1660320408000,
end: 1660343797000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 2',
start: 1660406808000,
end: 1660429160000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 3',
start: 1660493208000,
end: 1660503981000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 4',
start: 1660579608000,
end: 1660624108000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 5',
start: 1660666008000,
end: 1660681529000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
}
]
};
test.describe('Time List', () => {
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
page
}) => {
// Goto baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
const timelist = await test.step('Create a Time List', async () => {
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
expect(objectName).toBe(createdTimeList.name);
return createdTimeList;
});
await test.step('Create a Plan and add it to the timelist', async () => {
const createdPlan = await createPlanFromJSON(page, {
name: 'Test Plan',
json: testPlan
});
await page.goto(timelist.url);
// Expand the tree to show the plan
await page.click("button[title='Show selected item in tree']");
await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view');
await page.click("button[title='Save']");
await page.click("li[title='Save and Finish Editing']");
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
);
// Verify all events are displayed
const eventCount = await page.locator('.js-list-item').count();
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
});
await test.step('Does not show milliseconds in times', async () => {
// Get the first activity
const row = await page.locator('.js-list-item').first();
// Verify that none fo the times have milliseconds displayed.
// Example: 2024-11-17T16:00:00Z is correct and 2024-11-17T16:00:00.000Z is wrong
await expect(row.locator('.--start')).not.toContainText('.');
await expect(row.locator('.--end')).not.toContainText('.');
await expect(row.locator('.--duration')).not.toContainText('.');
});
});
});

View File

@ -41,8 +41,10 @@ import moment from 'moment';
import { v4 as uuid } from 'uuid';
const SCROLL_TIMEOUT = 10000;
const ROW_HEIGHT = 30;
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss:SSS';
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
const CURRENT_CSS_SUFFIX = '--is-current';
const PAST_CSS_SUFFIX = '--is-past';
const FUTURE_CSS_SUFFIX = '--is-future';
const headerItems = [
{
defaultDirection: true,
@ -79,9 +81,9 @@ const headerItems = [
format: function (value) {
let result;
if (value < 0) {
result = `-${getPreciseDuration(Math.abs(value))}`;
result = `+${getPreciseDuration(Math.abs(value), true)}`;
} else if (value > 0) {
result = `+${getPreciseDuration(value)}`;
result = `-${getPreciseDuration(value, true)}`;
} else {
result = 'Now';
}
@ -360,11 +362,12 @@ export default {
groups.forEach((key) => {
activities = activities.concat(this.planData[key]);
});
activities = activities.filter(this.filterActivities);
// filter activities first, then sort by start time
activities = activities.filter(this.filterActivities).sort(this.sortByStartTime);
activities = this.applyStyles(activities);
this.setScrollTop();
// sort by start time
this.planActivities = activities.sort(this.sortByStartTime);
this.planActivities = activities;
//We need to wait for the next tick since we need the height of the row from the DOM
this.$nextTick(this.setScrollTop);
},
updateTimeStampAndListActivities(time) {
this.timestamp = time;
@ -410,30 +413,41 @@ export default {
},
applyStyles(activities) {
let firstCurrentActivityIndex = -1;
let activityClosestToNowIndex = -1;
let currentActivitiesCount = 0;
const styledActivities = activities.map((activity, index) => {
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
activity.cssClass = '--is-current';
activity.cssClass = CURRENT_CSS_SUFFIX;
if (firstCurrentActivityIndex < 0) {
firstCurrentActivityIndex = index;
}
currentActivitiesCount = currentActivitiesCount + 1;
} else if (this.timestamp < activity.start) {
activity.cssClass = '--is-future';
activity.cssClass = FUTURE_CSS_SUFFIX;
//the index of the first activity that's greater than the current timestamp
if (activityClosestToNowIndex < 0) {
activityClosestToNowIndex = index;
}
} else {
activity.cssClass = '--is-past';
activity.cssClass = PAST_CSS_SUFFIX;
}
if (!activity.key) {
activity.key = uuid();
}
activity.duration = activity.start - this.timestamp;
if (activity.start < this.timestamp) {
//if the activity start time has passed, display the time to the end of the activity
activity.duration = activity.end - this.timestamp;
} else {
activity.duration = activity.start - this.timestamp;
}
return activity;
});
this.activityClosestToNowIndex = activityClosestToNowIndex;
this.firstCurrentActivityIndex = firstCurrentActivityIndex;
this.currentActivitiesCount = currentActivitiesCount;
@ -451,13 +465,22 @@ export default {
}
this.firstCurrentActivityIndex = -1;
this.activityClosestToNowIndex = -1;
this.currentActivitiesCount = 0;
this.$el.parentElement?.scrollTo({ top: 0 });
this.autoScrolled = false;
},
setScrollTop() {
//scroll to somewhere mid-way of the current activities
if (this.firstCurrentActivityIndex > -1) {
//The view isn't ready yet
if (!this.$el.parentElement) {
return;
}
const row = this.$el.querySelector('.js-list-item');
if (row && this.firstCurrentActivityIndex > -1) {
// scroll to somewhere mid-way of the current activities
const ROW_HEIGHT = row.getBoundingClientRect().height;
if (this.canAutoScroll() === false) {
return;
}
@ -469,7 +492,22 @@ export default {
behavior: 'smooth'
});
this.autoScrolled = false;
} else if (row && this.activityClosestToNowIndex > -1) {
// scroll to somewhere close to 'now'
const ROW_HEIGHT = row.getBoundingClientRect().height;
if (this.canAutoScroll() === false) {
return;
}
this.$el.parentElement.scrollTo({
top: ROW_HEIGHT * (this.activityClosestToNowIndex - 1),
behavior: 'smooth'
});
this.autoScrolled = false;
} else {
// scroll to the top
this.resetScroll();
}
},

View File

@ -219,10 +219,10 @@ describe('the plugin', function () {
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'
);
expect(itemValues[0].innerHTML.trim()).toEqual(
`${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`
`${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss')}Z`
);
expect(itemValues[1].innerHTML.trim()).toEqual(
`${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`
`${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss')}Z`
);
done();

View File

@ -63,14 +63,16 @@ export function millisecondsToDHMS(numericDuration) {
return `${dhms ? '+' : ''} ${dhms}`;
}
export function getPreciseDuration(value) {
export function getPreciseDuration(value, excludeMilliSeconds) {
const ms = value || 0;
return [
const duration = [
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))),
toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND)))
].join(':');
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
];
if (!excludeMilliSeconds) {
duration.push(toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))));
}
return duration.join(':');
}