[Time] Conductors and API Enhancements (#6768)

* Fixed #4975 - Compact Time Conductor styling
* Fixed #5773 - Ubiquitous global clock
* Mode functionality added to TimeAPI
* TimeAPI modified to always have a ticking clock
* Mode dropdown added to independent and regular time conductors
* Overall conductor appearance modifications and enhancements
* TimeAPI methods deprecated with warnings
* Significant updates to markup, styling and behavior of main Time Conductor and independent version.


---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Shefali <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Scott Bell <scott@traclabs.com>
This commit is contained in:
Jamie V 2023-07-18 17:32:05 -07:00 committed by GitHub
parent 85974fc5f1
commit 42b545917c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 3959 additions and 1969 deletions

6
API.md
View File

@ -2,7 +2,7 @@
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Building Applications With Open MCT](#developing-applications-with-open-mct)
- [Developing Applications With Open MCT](#developing-applications-with-open-mct)
- [Scope and purpose of this document](#scope-and-purpose-of-this-document)
- [Building From Source](#building-from-source)
- [Starting an Open MCT application](#starting-an-open-mct-application)
@ -26,7 +26,7 @@
- [Value Hints](#value-hints)
- [The Time Conductor and Telemetry](#the-time-conductor-and-telemetry)
- [Telemetry Providers](#telemetry-providers)
- [Telemetry Requests and Responses.](#telemetry-requests-and-responses)
- [Telemetry Requests and Responses](#telemetry-requests-and-responses)
- [Request Strategies **draft**](#request-strategies-draft)
- [`latest` request strategy](#latest-request-strategy)
- [`minmax` request strategy](#minmax-request-strategy)
@ -873,6 +873,8 @@ function without any arguments.
#### Stopping an active clock
_As of July 2023, this method will be deprecated. Open MCT will always have a ticking clock._
The `stopClock` method can be used to stop an active clock, and to clear it. It
will stop the clock from ticking, and set the active clock to `undefined`.

View File

@ -314,7 +314,9 @@ async function _isInEditMode(page, identifier) {
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
await timeConductorMode.locator('.js-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
@ -353,23 +355,23 @@ async function setRealTimeMode(page) {
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, { hours, mins, secs }, offsetButton) {
await offsetButton.click();
async function setTimeConductorOffset(page, { hours, mins, secs }) {
// await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
await page.fill('.pr-time-input__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
await page.fill('.pr-time-input__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
await page.fill('.pr-time-input__secs', secs);
}
// Click the check button
await page.locator('.pr-time__buttons .icon-check').click();
await page.locator('.pr-time-input--buttons .icon-check').click();
}
/**
@ -378,8 +380,10 @@ async function setTimeConductorOffset(page, { hours, mins, secs }, offsetButton)
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
// Click 'mode' button
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
await setTimeConductorOffset(page, offset);
}
/**
@ -388,8 +392,10 @@ async function setStartOffset(page, offset) {
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
// Click 'mode' button
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
await setTimeConductorOffset(page, offset);
}
/**

View File

@ -5,18 +5,18 @@
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1658617611983,\"end\":1658619411983}]}"
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},\"58f55f3a-46d9-4c37-a726-27b5d38b895a\":{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400878,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400878},\"19f2e461-190e-4662-8d62-251e90bb7aac\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433}}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619412848,\"modified\":1658619412848},\"7fa5749b-8969-494c-9d85-c272516d333c\":{\"identifier\":{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}}]},\"modified\":1658619413566,\"location\":\"mine\",\"persisted\":1658619413567},\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1658619413552,\"location\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"persisted\":1658619413552}}"
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"domainObject\":{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436}},{\"objectPath\":[{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433},{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/58f55f3a-46d9-4c37-a726-27b5d38b895a/19f2e461-190e-4662-8d62-251e90bb7aac\",\"domainObject\":{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654}}]"
},
{
"name": "mct-tree-expanded",
"value": "[\"/browse/mine\"]"
"value": "[]"
}
]
}
]
}
}

View File

@ -4,19 +4,23 @@
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1658617494563,\"end\":1658619294563},{\"start\":1658617090044,\"end\":1658618890044},{\"start\":1658616460484,\"end\":1658618260484},{\"start\":1658608882159,\"end\":1658610682159},{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619295366,\"modified\":1658619295366},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702},\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2b6bf89f-877b-42b8-acc1-a9a575efdbe1\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658610682787,\"location\":\"mine\",\"persisted\":1658610682787},\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"b9a9c413-4b94-401d-b0c7-5e404f182616\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618261112,\"location\":\"mine\",\"persisted\":1658618261112},\"3e294eae-6124-409b-a870-554d1bdcdd6f\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"108043b1-9c88-4e1d-8deb-fbf2cdb528f9\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618890910,\"location\":\"mine\",\"persisted\":1658618890910},\"ec24d05d-5df5-4c96-9241-b73636cd19a9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4062bd9b-b788-43dd-ab0a-8fa10a78d4b3\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658619295363,\"location\":\"mine\",\"persisted\":1658619295363}}"
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702},\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2b6bf89f-877b-42b8-acc1-a9a575efdbe1\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658610682787,\"location\":\"mine\",\"persisted\":1658610682787},\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"b9a9c413-4b94-401d-b0c7-5e404f182616\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618261112,\"location\":\"mine\",\"persisted\":1658618261112},\"3e294eae-6124-409b-a870-554d1bdcdd6f\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"108043b1-9c88-4e1d-8deb-fbf2cdb528f9\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618890910,\"location\":\"mine\",\"persisted\":1658618890910},\"ec24d05d-5df5-4c96-9241-b73636cd19a9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4062bd9b-b788-43dd-ab0a-8fa10a78d4b3\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658619295363,\"location\":\"mine\",\"persisted\":1658619295363},\"0ec517e8-6c11-4d98-89b5-c300fe61b304\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
},
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1658617494563,\"end\":1658619294563},{\"start\":1658617090044,\"end\":1658618890044},{\"start\":1658616460484,\"end\":1658618260484},{\"start\":1658608882159,\"end\":1658610682159},{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}"
},
{
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"domainObject\":{\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554}}]"
}
]
}
]
}
}

View File

@ -206,6 +206,49 @@ test.describe('Display Layout', () => {
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
});
test('independent time works with display layouts and its children', async ({ page }) => {
await setFixedTimeMode(page);
// Create Example Imagery
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
type: 'Example Imagery'
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
// Edit Display Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Display Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(exampleImageryObject.name)
});
let layoutGridHolder = page.locator('.l-layout__grid-holder');
await exampleImageryTreeItem.dragTo(layoutGridHolder);
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// flip on independent time conductor
await page.getByTitle('Enable independent Time Conductor').first().locator('label').click();
await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z');
await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z');
await page.getByRole('textbox').nth(1).click();
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// flip it off
await page.getByTitle('Disable independent Time Conductor').first().locator('label').click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
});
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({
page
}) => {

View File

@ -158,4 +158,46 @@ test.describe('Flexible Layout', () => {
// Verify that the item has been removed from the layout
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
});
test('independent time works with flexible layouts and its children', async ({ page }) => {
// Create Example Imagery
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
type: 'Example Imagery'
});
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Display Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(exampleImageryObject.name)
});
// Add the Sine Wave Generator to the Flexible Layout and save changes
await exampleImageryTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// flip on independent time conductor
await page.getByTitle('Enable independent Time Conductor').first().locator('label').click();
await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z');
await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z');
await page.getByRole('textbox').nth(1).click();
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// flip it off
await page.getByTitle('Disable independent Time Conductor').first().locator('label').click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
});
});

View File

@ -70,6 +70,52 @@ test.describe('Example Imagery Object', () => {
await dragContrastSliderAndAssertFilterValues(page);
});
test('Can use independent time conductor to change time', async ({ page }) => {
// Test independent fixed time with global fixed time
// flip on independent time conductor
await page.getByTitle('Enable independent Time Conductor').locator('label').click();
await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z');
await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z');
await page.getByRole('textbox').nth(1).click();
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// flip it off
await page.getByTitle('Disable independent Time Conductor').locator('label').click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
// Test independent fixed time with global realtime
await page.getByRole('button', { name: /Fixed Timespan/ }).click();
await page.getByTestId('conductor-modeOption-realtime').click();
await page.getByTitle('Enable independent Time Conductor').locator('label').click();
// check image date to be in the past
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// flip it off
await page.getByTitle('Disable independent Time Conductor').locator('label').click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
// Test independent realtime with global realtime
await page.getByTitle('Enable independent Time Conductor').locator('label').click();
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// change independent time to realtime
await page.getByRole('button', { name: /Fixed Timespan/ }).click();
await page.getByRole('menuitem', { name: /Local Clock/ }).click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
// back to the past
await page
.getByRole('button', { name: /Local Clock/ })
.first()
.click();
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
// check image date to be in the past
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
});
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
const deltaYStep = 100; //equivalent to 1x zoom
@ -189,11 +235,9 @@ test.describe('Example Imagery Object', () => {
test('Using the zoom features does not pause telemetry', async ({ page }) => {
const pausePlayButton = page.locator('.c-button.pause-play');
// open the time conductor drop down
await page.locator('.c-mode-button').click();
// switch to realtime
await setRealTimeMode(page);
// Click local clock
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
// Zoom in via button
@ -233,11 +277,8 @@ test.describe('Example Imagery in Display Layout', () => {
description: 'https://github.com/nasa/openmct/issues/3647'
});
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// set realtime mode
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await setRealTimeMode(page);
// pause/play button
const pausePlayButton = await page.locator('.c-button.pause-play');
@ -259,11 +300,8 @@ test.describe('Example Imagery in Display Layout', () => {
description: 'https://github.com/nasa/openmct/issues/3647'
});
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// set realtime mode
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await setRealTimeMode(page);
// pause/play button
const pausePlayButton = await page.locator('.c-button.pause-play');
@ -544,11 +582,8 @@ async function performImageryViewOperationsAndAssert(page) {
const nextImageButton = page.locator('.c-nav--next');
await nextImageButton.click();
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// Select local clock mode
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// set realtime mode
await setRealTimeMode(page);
// Zoom in on next image
await mouseZoomOnImageAndAssert(page, 2);
@ -893,3 +928,15 @@ async function createImageryView(page) {
page.waitForSelector('.c-message-banner__message')
]);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await page.locator('.c-compact-tc').click();
await page.waitForSelector('.c-tc-input-popup', { state: 'visible' });
// Click mode dropdown
await page.getByRole('button', { name: ' Fixed Timespan ' }).click();
// Click realtime
await page.getByTestId('conductor-modeOption-realtime').click();
}

View File

@ -156,9 +156,9 @@ export default function () {
key: 'thumbnail',
...formatThumbnail
});
openmct.telemetry.addProvider(getRealtimeProvider());
openmct.telemetry.addProvider(getHistoricalProvider());
openmct.telemetry.addProvider(getLadProvider());
openmct.telemetry.addProvider(getRealtimeProvider(openmct));
openmct.telemetry.addProvider(getHistoricalProvider(openmct));
openmct.telemetry.addProvider(getLadProvider(openmct));
};
}
@ -207,14 +207,14 @@ function getImageLoadDelay(domainObject) {
return imageLoadDelay;
}
function getRealtimeProvider() {
function getRealtimeProvider(openmct) {
return {
supportsSubscribe: (domainObject) => domainObject.type === 'example.imagery',
subscribe: (domainObject, callback) => {
const delay = getImageLoadDelay(domainObject);
const interval = setInterval(() => {
const imageSamples = getImageSamples(domainObject.configuration);
const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay);
const datum = pointForTimestamp(openmct.time.now(), domainObject.name, imageSamples, delay);
callback(datum);
}, delay);
@ -225,7 +225,7 @@ function getRealtimeProvider() {
};
}
function getHistoricalProvider() {
function getHistoricalProvider(openmct) {
return {
supportsRequest: (domainObject, options) => {
return domainObject.type === 'example.imagery' && options.strategy !== 'latest';
@ -233,17 +233,12 @@ function getHistoricalProvider() {
request: (domainObject, options) => {
const delay = getImageLoadDelay(domainObject);
let start = options.start;
const end = Math.min(options.end, Date.now());
const end = Math.min(options.end, openmct.time.now());
const data = [];
while (start <= end && data.length < delay) {
data.push(
pointForTimestamp(
start,
domainObject.name,
getImageSamples(domainObject.configuration),
delay
)
);
const imageSamples = getImageSamples(domainObject.configuration);
const generatedDataPoint = pointForTimestamp(start, domainObject.name, imageSamples, delay);
data.push(generatedDataPoint);
start += delay;
}
@ -252,7 +247,7 @@ function getHistoricalProvider() {
};
}
function getLadProvider() {
function getLadProvider(openmct) {
return {
supportsRequest: (domainObject, options) => {
return domainObject.type === 'example.imagery' && options.strategy === 'latest';
@ -260,7 +255,7 @@ function getLadProvider() {
request: (domainObject, options) => {
const delay = getImageLoadDelay(domainObject);
const datum = pointForTimestamp(
Date.now(),
openmct.time.now(),
domainObject.name,
getImageSamples(domainObject.configuration),
delay

View File

@ -96,6 +96,7 @@ define([
};
this.destroy = this.destroy.bind(this);
this.defaultClock = 'local';
[
/**
* Tracks current selection state of the application.
@ -353,6 +354,10 @@ define([
this.element = domElement;
if (!this.time.getClock()) {
this.time.setClock(this.defaultClock);
}
this.router.route(/^\/$/, () => {
this.router.setPath('/browse/');
});

View File

@ -58,7 +58,6 @@
:key="action.name"
role="menuitem"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"

View File

@ -204,27 +204,23 @@ export default class TelemetryAPI {
*/
standardizeRequestOptions(options = {}) {
if (!Object.hasOwn(options, 'start')) {
if (options.timeContext?.bounds()) {
options.start = options.timeContext.bounds().start;
if (options.timeContext?.getBounds()) {
options.start = options.timeContext.getBounds().start;
} else {
options.start = this.openmct.time.bounds().start;
options.start = this.openmct.time.getBounds().start;
}
}
if (!Object.hasOwn(options, 'end')) {
if (options.timeContext?.bounds()) {
options.end = options.timeContext.bounds().end;
if (options.timeContext?.getBounds()) {
options.end = options.timeContext.getBounds().end;
} else {
options.end = this.openmct.time.bounds().end;
options.end = this.openmct.time.getBounds().end;
}
}
if (!Object.hasOwn(options, 'domain')) {
options.domain = this.openmct.time.timeSystem().key;
}
if (!Object.hasOwn(options, 'timeContext')) {
options.timeContext = this.openmct.time;
options.domain = this.openmct.time.getTimeSystem().key;
}
return options;

View File

@ -29,15 +29,20 @@ describe('Telemetry API', () => {
beforeEach(() => {
openmct = {
time: jasmine.createSpyObj('timeAPI', ['timeSystem', 'bounds']),
time: jasmine.createSpyObj('timeAPI', ['timeSystem', 'getTimeSystem', 'bounds', 'getBounds']),
types: jasmine.createSpyObj('typeRegistry', ['get'])
};
openmct.time.timeSystem.and.returnValue({ key: 'system' });
openmct.time.getTimeSystem.and.returnValue({ key: 'system' });
openmct.time.bounds.and.returnValue({
start: 0,
end: 1
});
openmct.time.getBounds.and.returnValue({
start: 0,
end: 1
});
telemetryAPI = new TelemetryAPI(openmct);
});
@ -261,16 +266,14 @@ describe('Telemetry API', () => {
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
domain: 'system'
});
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
domain: 'system'
});
telemetryProvider.supportsRequest.calls.reset();
@ -281,16 +284,14 @@ describe('Telemetry API', () => {
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
domain: 'system'
});
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
domain: 'system'
});
});
@ -309,16 +310,14 @@ describe('Telemetry API', () => {
start: 20,
end: 30,
domain: 'someDomain',
signal,
timeContext: jasmine.any(Object)
signal
});
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
start: 20,
end: 30,
domain: 'someDomain',
signal,
timeContext: jasmine.any(Object)
signal
});
});
});

View File

@ -23,6 +23,7 @@
import _ from 'lodash';
import EventEmitter from 'EventEmitter';
import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants';
import { TIME_CONTEXT_EVENTS } from '../time/constants';
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
@ -60,8 +61,11 @@ export default class TelemetryCollection extends EventEmitter {
this.futureBuffer = [];
this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
if (!Object.hasOwn(options, 'timeContext')) {
options.timeContext = this.openmct.time;
}
this.options = options;
this.unsubscribe = undefined;
this.options = this.openmct.telemetry.standardizeRequestOptions(options);
this.pageState = undefined;
this.lastBounds = undefined;
this.requestAbort = undefined;
@ -78,11 +82,11 @@ export default class TelemetryCollection extends EventEmitter {
this._error(LOADED_ERROR);
}
this._setTimeSystem(this.options.timeContext.timeSystem());
this.lastBounds = this.options.timeContext.bounds();
this._setTimeSystem(this.options.timeContext.getTimeSystem());
this.lastBounds = this.options.timeContext.getBounds();
this._watchBounds();
this._watchTimeSystem();
this._watchTimeModeChange();
this._requestHistoricalTelemetry();
this._initiateSubscriptionTelemetry();
@ -101,6 +105,7 @@ export default class TelemetryCollection extends EventEmitter {
this._unwatchBounds();
this._unwatchTimeSystem();
this._unwatchTimeModeChange();
if (this.unsubscribe) {
this.unsubscribe();
}
@ -121,7 +126,7 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
async _requestHistoricalTelemetry() {
let options = { ...this.options };
let options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });
const historicalProvider = this.openmct.telemetry.findRequestProvider(
this.domainObject,
options
@ -433,6 +438,10 @@ export default class TelemetryCollection extends EventEmitter {
this._reset();
}
_timeModeChanged() {
this._reset();
}
/**
* Reset the telemetry data of the collection, and re-request
* historical telemetry
@ -450,19 +459,35 @@ export default class TelemetryCollection extends EventEmitter {
}
/**
* adds the _bounds callback to the 'bounds' timeAPI listener
* adds the _bounds callback to the 'boundsChanged' timeAPI listener
* @private
*/
_watchBounds() {
this.options.timeContext.on('bounds', this._bounds, this);
this.options.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this._bounds, this);
}
/**
* removes the _bounds callback from the 'bounds' timeAPI listener
* removes the _bounds callback from the 'boundsChanged' timeAPI listener
* @private
*/
_unwatchBounds() {
this.options.timeContext.off('bounds', this._bounds, this);
this.options.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this._bounds, this);
}
/**
* adds the _timeModeChanged callback to the 'modeChanged' timeAPI listener
* @private
*/
_watchTimeModeChange() {
this.options.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this._timeModeChanged, this);
}
/**
* removes the _timeModeChanged callback from the 'modeChanged' timeAPI listener
* @private
*/
_unwatchTimeModeChange() {
this.options.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this._timeModeChanged, this);
}
/**
@ -470,7 +495,11 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
_watchTimeSystem() {
this.options.timeContext.on('timeSystem', this._setTimeSystemAndFetchData, this);
this.options.timeContext.on(
TIME_CONTEXT_EVENTS.timeSystemChanged,
this._setTimeSystemAndFetchData,
this
);
}
/**
@ -478,7 +507,11 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
_unwatchTimeSystem() {
this.options.timeContext.off('timeSystem', this._setTimeSystemAndFetchData, this);
this.options.timeContext.off(
TIME_CONTEXT_EVENTS.timeSystemChanged,
this._setTimeSystemAndFetchData,
this
);
}
/**

View File

@ -20,7 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext, { TIME_CONTEXT_EVENTS } from './TimeContext';
import TimeContext from './TimeContext';
import { MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants';
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
@ -46,7 +47,7 @@ class IndependentTimeContext extends TimeContext {
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
}
bounds(newBounds) {
bounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
} else {
@ -54,7 +55,23 @@ class IndependentTimeContext extends TimeContext {
}
}
tick(timestamp) {
getBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getBounds();
} else {
return super.getBounds();
}
}
setBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setBounds(...arguments);
} else {
return super.setBounds(...arguments);
}
}
tick() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
} else {
@ -62,7 +79,7 @@ class IndependentTimeContext extends TimeContext {
}
}
clockOffsets(offsets) {
clockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
} else {
@ -70,11 +87,19 @@ class IndependentTimeContext extends TimeContext {
}
}
stopClock() {
getClockOffsets() {
if (this.upstreamTimeContext) {
this.upstreamTimeContext.stopClock();
return this.upstreamTimeContext.getClockOffsets();
} else {
super.stopClock();
return super.getClockOffsets();
}
}
setClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClockOffsets(...arguments);
} else {
return super.setClockOffsets(...arguments);
}
}
@ -86,10 +111,19 @@ class IndependentTimeContext extends TimeContext {
return this.globalTimeContext.timeSystem(...arguments);
}
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.globalTimeContext.getTimeSystem();
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
* and ticking will begin. Offsets from 'now' must also be provided.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
@ -126,15 +160,19 @@ class IndependentTimeContext extends TimeContext {
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit('clock', this.activeClock);
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (this.activeClock !== undefined) {
//set the mode here or isRealtime will be false even if we're in clock mode
this.setMode(REALTIME_MODE_KEY);
this.clockOffsets(offsets);
this.activeClock.on('tick', this.tick);
}
@ -145,6 +183,122 @@ class IndependentTimeContext extends TimeContext {
return this.activeClock;
}
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClock();
}
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClock(...arguments);
}
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
}
}
const previousClock = this.activeClock;
if (previousClock) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
this.activeClock.on('tick', this.tick);
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
return this.activeClock;
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getMode();
}
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @param {TimeBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode, offsetsOrBounds) {
if (!mode) {
return;
}
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setMode(...arguments);
}
if (mode === MODES.realtime && this.activeClock === undefined) {
throw `Unknown clock. Has a clock been registered with 'addClock'?`;
}
if (mode !== this.mode) {
this.mode = mode;
/**
* The active mode has changed.
* @event modeChanged
* @memberof module:openmct.TimeAPI~
* @property {Mode} mode The newly activated mode
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
}
//We are also going to set bounds here
if (offsetsOrBounds !== undefined) {
if (this.mode === REALTIME_MODE_KEY) {
this.setClockOffsets(offsetsOrBounds);
} else {
this.setBounds(offsetsOrBounds);
}
}
return this.mode;
}
/**
* Causes this time context to follow another time context (either the global context, or another upstream time context)
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
@ -152,7 +306,7 @@ class IndependentTimeContext extends TimeContext {
followTimeContext() {
this.stopFollowingTimeContext();
if (this.upstreamTimeContext) {
TIME_CONTEXT_EVENTS.forEach((eventName) => {
Object.values(TIME_CONTEXT_EVENTS).forEach((eventName) => {
const thisTimeContext = this;
this.upstreamTimeContext.on(eventName, passthrough);
this.unlisteners.push(() => {
@ -197,6 +351,7 @@ class IndependentTimeContext extends TimeContext {
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
}
hasOwnContext() {
@ -259,11 +414,16 @@ class IndependentTimeContext extends TimeContext {
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
this.emit('bounds', this.getBounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
this.globalTimeContext.emit('refreshContext', viewKey);
}
}
#copy(object) {
return JSON.parse(JSON.stringify(object));
}
}
export default IndependentTimeContext;

View File

@ -22,6 +22,7 @@
import GlobalTimeContext from './GlobalTimeContext';
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
/**
* The public API for setting and querying the temporal state of the
@ -134,14 +135,15 @@ class TimeAPI extends GlobalTimeContext {
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
//stop following upstream time context since the view has it's own
timeContext.resetContext();
if (clockKey) {
timeContext.clock(clockKey, value);
timeContext.setClock(clockKey);
timeContext.setMode(REALTIME_MODE_KEY, value);
} else {
timeContext.stopClock();
timeContext.bounds(value);
timeContext.setMode(FIXED_MODE_KEY, value);
}
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
@ -185,6 +187,7 @@ class TimeAPI extends GlobalTimeContext {
}
let viewTimeContext = this.getIndependentContext(viewKey);
if (!viewTimeContext) {
// If the context doesn't exist yet, create it.
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);

View File

@ -87,7 +87,7 @@ describe('The Time API', function () {
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
expect(api.timeSystem()).toEqual(timeSystem);
});
it('Disallows setting of time system without bounds', function () {
@ -110,7 +110,7 @@ describe('The Time API', function () {
expect(function () {
api.timeSystem(timeSystemKey);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
expect(api.timeSystem()).toEqual(timeSystem);
});
it('Emits an event when time system changes', function () {
@ -202,12 +202,12 @@ describe('The Time API', function () {
expect(mockTickSource.off).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
it('Allows the active clock to be set and unset', function () {
xit('Allows the active clock to be set and unset', function () {
expect(api.clock()).toBeUndefined();
api.clock('mts', mockOffsets);
expect(api.clock()).toBeDefined();
api.stopClock();
expect(api.clock()).toBeUndefined();
// api.stopClock();
// expect(api.clock()).toBeUndefined();
});
it('Provides a default time context', () => {

View File

@ -21,8 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
export const TIME_CONTEXT_EVENTS = ['bounds', 'clock', 'timeSystem', 'clockOffsets'];
import { TIME_CONTEXT_EVENTS, MODES, REALTIME_MODE_KEY, FIXED_MODE_KEY } from './constants';
class TimeContext extends EventEmitter {
constructor() {
@ -42,6 +41,7 @@ class TimeContext extends EventEmitter {
this.activeClock = undefined;
this.offsets = undefined;
this.mode = undefined;
this.tick = this.tick.bind(this);
}
@ -56,6 +56,8 @@ class TimeContext extends EventEmitter {
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
this.#warnMethodDeprecated('"timeSystem"', '"getTimeSystem" and "setTimeSystem"');
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error('Must specify bounds when changing time system without an active clock.');
@ -91,7 +93,7 @@ class TimeContext extends EventEmitter {
throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';
}
this.system = timeSystem;
this.system = this.#copy(timeSystem);
/**
* The time system used by the time
@ -102,7 +104,10 @@ class TimeContext extends EventEmitter {
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
const system = this.#copy(this.system);
this.emit('timeSystem', system);
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, system);
if (bounds) {
this.bounds(bounds);
}
@ -163,6 +168,8 @@ class TimeContext extends EventEmitter {
* @method bounds
*/
bounds(newBounds) {
this.#warnMethodDeprecated('"bounds"', '"getBounds" and "setBounds"');
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
@ -170,7 +177,7 @@ class TimeContext extends EventEmitter {
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
this.boundsVal = this.#copy(newBounds);
/**
* The start time, end time, or both have been updated.
* @event bounds
@ -180,10 +187,11 @@ class TimeContext extends EventEmitter {
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
return this.#copy(this.boundsVal);
}
/**
@ -248,6 +256,8 @@ class TimeContext extends EventEmitter {
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
this.#warnMethodDeprecated('"clockOffsets"', '"getClockOffsets" and "setClockOffsets"');
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
@ -278,20 +288,19 @@ class TimeContext extends EventEmitter {
}
/**
* Stop the currently active clock from ticking, and unset it. This will
* Stop following the currently active clock. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
if (this.activeClock) {
this.clock(undefined, undefined);
}
this.#warnMethodDeprecated('"stopClock"');
this.setMode(FIXED_MODE_KEY);
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
* and ticking will begin. Offsets from 'now' must also be provided.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
@ -301,6 +310,8 @@ class TimeContext extends EventEmitter {
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
this.#warnMethodDeprecated('"clock"', '"getClock" and "setClock"');
if (arguments.length === 2) {
let clock;
@ -324,15 +335,19 @@ class TimeContext extends EventEmitter {
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit('clock', this.activeClock);
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (this.activeClock !== undefined) {
//set the mode or isRealtime will be false even though we're in clock mode
this.setMode(REALTIME_MODE_KEY);
this.clockOffsets(offsets);
this.activeClock.on('tick', this.tick);
}
@ -340,7 +355,7 @@ class TimeContext extends EventEmitter {
throw 'When setting the clock, clock offsets must also be provided';
}
return this.activeClock;
return this.isRealTime() ? this.activeClock : undefined;
}
/**
@ -349,29 +364,304 @@ class TimeContext extends EventEmitter {
* using current offsets.
*/
tick(timestamp) {
if (!this.activeClock) {
return;
// always emit the timestamp
this.emit('tick', timestamp);
if (this.mode === REALTIME_MODE_KEY) {
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
// "bounds" will be deprecated in a future release
this.emit('bounds', this.boundsVal, true);
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, true);
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
/**
* Checks if this time context is in real-time mode or not.
* Get the timestamp of the current clock
* @returns {number} current timestamp of current clock regardless of mode
* @memberof module:openmct.TimeAPI#
* @method now
*/
now() {
return this.activeClock.currentValue();
}
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.system;
}
/**
* Set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method setTimeSystem
*/
setTimeSystem(timeSystemOrKey, bounds) {
if (timeSystemOrKey === undefined) {
throw 'Please provide a time system';
}
let timeSystem;
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw `Unknown time system ${timeSystemOrKey}. Has it been registered with 'addTimeSystem'?`;
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw `Unknown time system ${timeSystemOrKey.key}. Has it been registered with 'addTimeSystem'?`;
}
} else {
throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';
}
this.system = this.#copy(timeSystem);
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, this.#copy(this.system));
this.emit('timeSystem', this.#copy(this.system));
if (bounds) {
this.setBounds(bounds);
}
}
/**
* Get the start and end time of the time conductor. Basic validation
* of bounds is performed.
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
getBounds() {
//Return a copy to prevent direct mutation of time conductor bounds.
return this.#copy(this.boundsVal);
}
/**
* Set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
setBounds(newBounds) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = this.#copy(newBounds);
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (i.e. was an automatic update), false otherwise.
*/
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);
this.emit('bounds', this.boundsVal, false);
}
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
}
}
const previousClock = this.activeClock;
if (previousClock) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
this.activeClock.on('tick', this.tick);
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
this.emit('clock', this.activeClock);
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @param {TimeBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode, offsetsOrBounds) {
if (!mode) {
return;
}
if (mode === MODES.realtime && this.activeClock === undefined) {
throw `Unknown clock. Has a clock been registered with 'addClock'?`;
}
if (mode !== this.mode) {
this.mode = mode;
/**
* The active mode has changed.
* @event modeChanged
* @memberof module:openmct.TimeAPI~
* @property {Mode} mode The newly activated mode
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
}
if (offsetsOrBounds !== undefined) {
if (this.isRealTime()) {
this.setClockOffsets(offsetsOrBounds);
} else {
this.setBounds(offsetsOrBounds);
}
}
}
/**
* Checks if this time context is in realtime mode or not.
* @returns {boolean} true if this context is in real-time mode, false if not
*/
isRealTime() {
if (this.clock()) {
return true;
return this.mode === MODES.realtime;
}
/**
* Checks if this time context is in fixed mode or not.
* @returns {boolean} true if this context is in fixed mode, false if not
*/
isFixed() {
return this.mode === MODES.fixed;
}
/**
* Get the currently applied clock offsets.
* @returns {ClockOffsets}
*/
getClockOffsets() {
return this.offsets;
}
/**
* Set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
setClockOffsets(offsets) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
return false;
this.offsets = this.#copy(offsets);
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.setBounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.#copy(offsets));
}
#warnMethodDeprecated(method, newMethod) {
let message = `[DEPRECATION WARNING]: The ${method} API method is deprecated and will be removed in a future version of Open MCT.`;
if (newMethod) {
message += ` Please use the ${newMethod} API method(s) instead.`;
}
// TODO: add docs and point to them in warning.
// For more information and migration instructions, visit [link to documentation or migration guide].
console.warn(message);
}
#copy(object) {
return JSON.parse(JSON.stringify(object));
}
}

22
src/api/time/constants.js Normal file
View File

@ -0,0 +1,22 @@
export const TIME_CONTEXT_EVENTS = {
//old API events - to be deprecated
bounds: 'bounds',
clock: 'clock',
timeSystem: 'timeSystem',
clockOffsets: 'clockOffsets',
//new API events
tick: 'tick',
modeChanged: 'modeChanged',
boundsChanged: 'boundsChanged',
clockChanged: 'clockChanged',
timeSystemChanged: 'timeSystemChanged',
clockOffsetsChanged: 'clockOffsetsChanged'
};
export const REALTIME_MODE_KEY = 'realtime';
export const FIXED_MODE_KEY = 'fixed';
export const MODES = {
[FIXED_MODE_KEY]: FIXED_MODE_KEY,
[REALTIME_MODE_KEY]: REALTIME_MODE_KEY
};

View File

@ -20,14 +20,20 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets'];
import { FIXED_MODE_KEY, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from '../../api/time/constants';
const SEARCH_MODE = 'tc.mode';
const SEARCH_TIME_SYSTEM = 'tc.timeSystem';
const SEARCH_START_BOUND = 'tc.startBound';
const SEARCH_END_BOUND = 'tc.endBound';
const SEARCH_START_DELTA = 'tc.startDelta';
const SEARCH_END_DELTA = 'tc.endDelta';
const MODE_FIXED = 'fixed';
const TIME_EVENTS = [
TIME_CONTEXT_EVENTS.timeSystemChanged,
TIME_CONTEXT_EVENTS.modeChanged,
TIME_CONTEXT_EVENTS.clockChanged,
TIME_CONTEXT_EVENTS.clockOffsetsChanged
];
export default class URLTimeSettingsSynchronizer {
constructor(openmct) {
@ -67,7 +73,7 @@ export default class URLTimeSettingsSynchronizer {
}
updateTimeSettings() {
let timeParameters = this.parseParametersFromUrl();
const timeParameters = this.parseParametersFromUrl();
if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters);
@ -78,21 +84,18 @@ export default class URLTimeSettingsSynchronizer {
}
parseParametersFromUrl() {
let searchParams = this.openmct.router.getAllSearchParams();
let mode = searchParams.get(SEARCH_MODE);
let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
let startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);
let endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
let bounds = {
const searchParams = this.openmct.router.getAllSearchParams();
const mode = searchParams.get(SEARCH_MODE);
const timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
const startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);
const endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
const bounds = {
start: startBound,
end: endBound
};
let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
let clockOffsets = {
const startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
const endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
const clockOffsets = {
start: 0 - startOffset,
end: endOffset
};
@ -106,30 +109,35 @@ export default class URLTimeSettingsSynchronizer {
}
setTimeApiFromUrl(timeParameters) {
if (timeParameters.mode === 'fixed') {
if (this.openmct.time.timeSystem().key !== timeParameters.timeSystem) {
this.openmct.time.timeSystem(timeParameters.timeSystem, timeParameters.bounds);
} else if (!this.areStartAndEndEqual(this.openmct.time.bounds(), timeParameters.bounds)) {
this.openmct.time.bounds(timeParameters.bounds);
}
const timeSystem = this.openmct.time.getTimeSystem();
if (this.openmct.time.clock()) {
this.openmct.time.stopClock();
if (timeParameters.mode === FIXED_MODE_KEY) {
// should update timesystem
if (timeSystem.key !== timeParameters.timeSystem) {
this.openmct.time.setTimeSystem(timeParameters.timeSystem, timeParameters.bounds);
}
if (!this.areStartAndEndEqual(this.openmct.time.getBounds(), timeParameters.bounds)) {
this.openmct.time.setMode(FIXED_MODE_KEY, timeParameters.bounds);
} else {
this.openmct.time.setMode(FIXED_MODE_KEY);
}
} else {
if (!this.openmct.time.clock() || this.openmct.time.clock().key !== timeParameters.mode) {
this.openmct.time.clock(timeParameters.mode, timeParameters.clockOffsets);
} else if (
!this.areStartAndEndEqual(this.openmct.time.clockOffsets(), timeParameters.clockOffsets)
) {
this.openmct.time.clockOffsets(timeParameters.clockOffsets);
const clock = this.openmct.time.getClock();
if (clock?.key !== timeParameters.mode) {
this.openmct.time.setClock(timeParameters.mode);
}
if (
!this.openmct.time.timeSystem() ||
this.openmct.time.timeSystem().key !== timeParameters.timeSystem
!this.areStartAndEndEqual(this.openmct.time.getClockOffsets(), timeParameters.clockOffsets)
) {
this.openmct.time.timeSystem(timeParameters.timeSystem);
this.openmct.time.setMode(REALTIME_MODE_KEY, timeParameters.clockOffsets);
} else {
this.openmct.time.setMode(REALTIME_MODE_KEY);
}
if (timeSystem?.key !== timeParameters.timeSystem) {
this.openmct.time.setTimeSystem(timeParameters.timeSystem);
}
}
}
@ -141,13 +149,14 @@ export default class URLTimeSettingsSynchronizer {
}
setUrlFromTimeApi() {
let searchParams = this.openmct.router.getAllSearchParams();
let clock = this.openmct.time.clock();
let bounds = this.openmct.time.bounds();
let clockOffsets = this.openmct.time.clockOffsets();
const searchParams = this.openmct.router.getAllSearchParams();
const clock = this.openmct.time.getClock();
const mode = this.openmct.time.getMode();
const bounds = this.openmct.time.getBounds();
const clockOffsets = this.openmct.time.getClockOffsets();
if (clock === undefined) {
searchParams.set(SEARCH_MODE, MODE_FIXED);
if (mode === FIXED_MODE_KEY) {
searchParams.set(SEARCH_MODE, FIXED_MODE_KEY);
searchParams.set(SEARCH_START_BOUND, bounds.start);
searchParams.set(SEARCH_END_BOUND, bounds.end);
@ -168,8 +177,8 @@ export default class URLTimeSettingsSynchronizer {
searchParams.delete(SEARCH_END_BOUND);
}
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key);
this.openmct.router.setAllSearchParams(searchParams);
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.getTimeSystem().key);
this.openmct.router.updateParams(searchParams);
}
areTimeParametersValid(timeParameters) {
@ -179,7 +188,7 @@ export default class URLTimeSettingsSynchronizer {
this.isModeValid(timeParameters.mode) &&
this.isTimeSystemValid(timeParameters.timeSystem)
) {
if (timeParameters.mode === 'fixed') {
if (timeParameters.mode === FIXED_MODE_KEY) {
isValid = this.areStartAndEndValid(timeParameters.bounds);
} else {
isValid = this.areStartAndEndValid(timeParameters.clockOffsets);
@ -203,8 +212,9 @@ export default class URLTimeSettingsSynchronizer {
isTimeSystemValid(timeSystem) {
let isValid = timeSystem !== undefined;
if (isValid) {
let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
const timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
isValid = timeSystemObject !== undefined;
}
@ -218,18 +228,17 @@ export default class URLTimeSettingsSynchronizer {
isValid = true;
}
if (isValid) {
if (mode.toLowerCase() === MODE_FIXED) {
isValid = true;
} else {
isValid = this.openmct.time.clocks.get(mode) !== undefined;
}
if (
isValid &&
(mode.toLowerCase() === FIXED_MODE_KEY || this.openmct.time.clocks.get(mode) !== undefined)
) {
isValid = true;
}
return isValid;
}
areStartAndEndEqual(firstBounds, secondBounds) {
return firstBounds.start === secondBounds.start && firstBounds.end === secondBounds.end;
return firstBounds?.start === secondBounds.start && firstBounds?.end === secondBounds.end;
}
}

View File

@ -40,7 +40,6 @@ describe('The URLTimeSettingsSynchronizer', () => {
});
afterEach(() => {
openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction);
appHolder = undefined;

View File

@ -41,13 +41,13 @@
<script>
import moment from 'moment';
import momentTimezone from 'moment-timezone';
import ticker from 'utils/clock/Ticker';
import raf from 'utils/raf';
export default {
inject: ['openmct', 'domainObject'],
data() {
return {
lastTimestamp: null
lastTimestamp: this.openmct.time.now()
};
},
computed: {
@ -85,12 +85,11 @@ export default {
}
},
mounted() {
this.unlisten = ticker.listen(this.tick);
this.tick = raf(this.tick);
this.openmct.time.on('tick', this.tick);
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
this.openmct.time.off('tick', this.tick);
},
methods: {
tick(timestamp) {

View File

@ -30,7 +30,7 @@
<script>
import moment from 'moment';
import ticker from 'utils/clock/Ticker';
import raf from 'utils/raf';
export default {
inject: ['openmct'],
@ -42,20 +42,22 @@ export default {
},
data() {
return {
timeTextValue: null
timeTextValue: this.openmct.time.now()
};
},
mounted() {
this.unlisten = ticker.listen(this.tick);
this.tick = raf(this.tick);
this.openmct.time.on('tick', this.tick);
this.tick(this.timeTextValue);
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
this.openmct.time.off('tick', this.tick);
},
methods: {
tick(timestamp) {
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} UTC`;
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} ${
this.openmct.time.getTimeSystem().name
}`;
}
}
};

View File

@ -98,6 +98,7 @@ describe('Clock plugin:', () => {
clockView.show(child);
await Vue.nextTick();
await new Promise((resolve) => requestAnimationFrame(resolve));
});
afterEach(() => {
@ -222,10 +223,12 @@ describe('Clock plugin:', () => {
it('contains text', async () => {
await setupClock(true);
await Vue.nextTick();
await new Promise((resolve) => requestAnimationFrame(resolve));
clockIndicator = openmct.indicators.indicatorObjects.find(
(indicator) => indicator.key === 'clock-indicator'
).element;
const clockIndicatorText = clockIndicator.textContent.trim();
const textIncludesUTC = clockIndicatorText.includes('UTC');

View File

@ -255,7 +255,7 @@ export default {
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
this.metadata = {};
this.requestCount = 0;
@ -375,14 +375,11 @@ export default {
return age < cutoff && !this.refreshCSS;
},
canTrackDuration() {
let hasClock;
if (this.timeContext) {
hasClock = this.timeContext.clock();
return this.timeContext.isRealTime();
} else {
hasClock = this.openmct.time.clock();
return this.openmct.time.isRealTime();
}
return hasClock && this.timeSystem.isUTCBased;
},
isNextDisabled() {
let disabled = false;
@ -531,14 +528,11 @@ export default {
return isFresh;
},
isFixed() {
let clock;
if (this.timeContext) {
clock = this.timeContext.clock();
return this.timeContext.isFixed();
} else {
clock = this.openmct.time.clock();
return this.openmct.time.isFixed();
}
return clock === undefined;
},
isSelectable() {
return true;
@ -1111,7 +1105,7 @@ export default {
window.clearInterval(this.durationTracker);
},
updateDuration() {
let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue();
let currentTime = this.timeContext.getClock().currentValue();
if (currentTime === undefined) {
this.numericDuration = currentTime;
} else if (Number.isInteger(this.parsedSelectedTime)) {

View File

@ -112,7 +112,6 @@ export default class RelatedTelemetry {
start: this._openmct.time.bounds().start,
end: this._parseTime(datum)
};
ephemeralContext.stopClock();
ephemeralContext.bounds(newBounds);
const options = {

View File

@ -24,15 +24,15 @@ const DEFAULT_DURATION_FORMATTER = 'duration';
const IMAGE_HINT_KEY = 'image';
const IMAGE_THUMBNAIL_HINT_KEY = 'thumbnail';
const IMAGE_DOWNLOAD_NAME_HINT_KEY = 'imageDownloadName';
import { TIME_CONTEXT_EVENTS } from '../../../api/time/constants';
export default {
inject: ['openmct', 'domainObject', 'objectPath'],
mounted() {
// listen
this.boundsChange = this.boundsChange.bind(this);
this.timeSystemChange = this.timeSystemChange.bind(this);
this.boundsChanged = this.boundsChanged.bind(this);
this.timeSystemChanged = this.timeSystemChanged.bind(this);
this.setDataTimeContext = this.setDataTimeContext.bind(this);
this.setDataTimeContext();
this.openmct.objectViews.on('clearData', this.dataCleared);
// Get metadata and formatters
@ -59,14 +59,8 @@ export default {
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
timeContext: this.timeContext
});
this.telemetryCollection.on('add', this.dataAdded);
this.telemetryCollection.on('remove', this.dataRemoved);
this.telemetryCollection.on('clear', this.dataCleared);
this.telemetryCollection.load();
this.setDataTimeContext();
this.loadTelemetry();
},
beforeDestroy() {
if (this.unsubscribe) {
@ -111,14 +105,13 @@ export default {
setDataTimeContext() {
this.stopFollowingDataTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('bounds', this.boundsChange);
this.boundsChange(this.timeContext.bounds());
this.timeContext.on('timeSystem', this.timeSystemChange);
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);
this.timeContext.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);
},
stopFollowingDataTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.boundsChange);
this.timeContext.off('timeSystem', this.timeSystemChange);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);
this.timeContext.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);
}
},
formatImageUrl(datum) {
@ -161,14 +154,23 @@ export default {
return this.timeFormatter.parse(datum);
},
boundsChange(bounds, isTick) {
loadTelemetry() {
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
timeContext: this.timeContext
});
this.telemetryCollection.on('add', this.dataAdded);
this.telemetryCollection.on('remove', this.dataRemoved);
this.telemetryCollection.on('clear', this.dataCleared);
this.telemetryCollection.load();
},
boundsChanged(bounds, isTick) {
if (isTick) {
return;
}
this.bounds = bounds; // setting bounds for ImageryView watcher
},
timeSystemChange() {
timeSystemChanged() {
this.timeSystem = this.timeContext.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);

View File

@ -684,6 +684,10 @@ describe('The Imagery View Layouts', () => {
return Vue.nextTick();
});
afterEach(() => {
openmct.time.setClock('local');
});
it('on mount should show imagery within the given bounds', (done) => {
Vue.nextTick(() => {
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');

View File

@ -422,7 +422,7 @@ export default {
});
},
filterAndSortEntries() {
const filterTime = Date.now();
const filterTime = this.openmct.time.now();
const pageEntries =
getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];

View File

@ -200,7 +200,7 @@ export default {
template: '<div id="snap-annotation"></div>'
}).$mount();
const painterroInstance = new PainterroInstance(annotateVue.$el);
const painterroInstance = new PainterroInstance(annotateVue.$el, this.openmct);
const annotateOverlay = this.openmct.overlays.overlay({
element: annotateVue.$el,
size: 'large',
@ -265,7 +265,6 @@ export default {
this.embed.bounds.start !== bounds.start || this.embed.bounds.end !== bounds.end;
const isFixedTimespanMode = !this.openmct.time.clock();
this.openmct.time.stopClock();
let message = '';
if (isTimeBoundChanged) {
this.openmct.time.bounds({

View File

@ -449,7 +449,7 @@ export default {
}
}
this.entry.modified = Date.now();
this.entry.modified = this.openmct.time.now();
this.$emit('updateEntry', this.entry);
},

View File

@ -23,7 +23,7 @@
<div class="c-snapshots-h">
<div class="l-browse-bar">
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w">
<div class="l-browse-bar__object-name--w c-snapshots-h__title">
<div class="l-browse-bar__object-name c-object-label">
<div class="c-object-label__type-icon icon-camera"></div>
<div class="c-object-label__name">Notebook Snapshots</div>

View File

@ -123,7 +123,7 @@ export async function createNewEmbed(snapshotMeta, snapshot = '') {
domainObjectType && domainObjectType.definition
? domainObjectType.definition.cssClass
: 'icon-object-unknown';
const date = Date.now();
const date = openmct.time.now();
const historicLink = link
? getHistoricLinkInFixedMode(openmct, bounds, link)
: objectLink.computed.objectLink.call({
@ -159,7 +159,7 @@ export async function addNotebookEntry(
return;
}
const date = Date.now();
const date = openmct.time.now();
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
const embeds = embed ? [embed] : [];

View File

@ -26,11 +26,12 @@ const DEFAULT_CONFIG = {
};
export default class PainterroInstance {
constructor(element) {
constructor(element, openmct) {
this.elementId = element.id;
this.isSave = false;
this.painterroInstance = undefined;
this.saveCallback = undefined;
this.openmct = openmct;
}
dismiss() {
@ -67,11 +68,11 @@ export default class PainterroInstance {
src: fullSizeImageURL,
type: url.type,
size: url.size,
modified: Date.now()
modified: this.openmct.time.now()
},
thumbnailImage: {
src: thumbnailURL,
modified: Date.now()
modified: this.openmct.time.now()
}
};

View File

@ -258,7 +258,7 @@ export default {
seriesModels: [],
legend: {},
pending: 0,
isRealTime: this.openmct.time.clock() !== undefined,
isRealTime: this.openmct.time.isRealTime(),
loaded: false,
isTimeOutOfSync: false,
isFrozenOnMouseDown: false,
@ -350,7 +350,7 @@ export default {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
eventHelpers.extend(this);
this.updateRealTime = this.updateRealTime.bind(this);
this.updateMode = this.updateMode.bind(this);
this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
this.setTimeContext = this.setTimeContext.bind(this);
@ -522,20 +522,19 @@ export default {
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.followTimeContext();
},
followTimeContext() {
this.updateDisplayBounds(this.timeContext.bounds());
this.timeContext.on('clock', this.updateRealTime);
this.timeContext.on('bounds', this.updateDisplayBounds);
this.updateDisplayBounds(this.timeContext.getBounds());
this.timeContext.on('modeChanged', this.updateMode);
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
this.synchronized(true);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('clock', this.updateRealTime);
this.timeContext.off('bounds', this.updateDisplayBounds);
this.timeContext.off('modeChanged', this.updateMode);
this.timeContext.off('boundsChanged', this.updateDisplayBounds);
}
},
getConfig() {
@ -774,8 +773,8 @@ export default {
const displayRange = series.getDisplayRange(xKey);
this.config.xAxis.set('range', displayRange);
},
updateRealTime(clock) {
this.isRealTime = clock !== undefined;
updateMode() {
this.isRealTime = this.timeContext.isRealTime();
},
/**
@ -836,13 +835,13 @@ export default {
* displays can update accordingly.
*/
synchronized(value) {
const isLocalClock = this.timeContext.clock();
const isRealTime = this.timeContext.isRealTime();
if (typeof value !== 'undefined') {
this._synchronized = value;
this.isTimeOutOfSync = value !== true;
const isUnsynced = isLocalClock && !value;
const isUnsynced = isRealTime && !value;
this.setStatus(isUnsynced);
}
@ -1867,7 +1866,6 @@ export default {
},
synchronizeTimeConductor() {
this.timeContext.stopClock();
const range = this.config.xAxis.get('displayRange');
this.timeContext.bounds({
start: range.min,

View File

@ -73,17 +73,17 @@ export default {
this.xAxis = this.getXAxisFromConfig();
this.loaded = true;
this.setUpXAxisOptions();
this.openmct.time.on('timeSystem', this.syncXAxisToTimeSystem);
this.openmct.time.on('timeSystemChanged', this.syncXAxisToTimeSystem);
this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions);
},
beforeDestroy() {
this.openmct.time.off('timeSystem', this.syncXAxisToTimeSystem);
this.openmct.time.off('timeSystemChanged', this.syncXAxisToTimeSystem);
},
methods: {
isEnabledXKeyToggle() {
const isSinglePlot = this.xKeyOptions && this.xKeyOptions.length > 1 && this.seriesModel;
const isFrozen = this.xAxis.get('frozen');
const inRealTimeMode = this.openmct.time.clock();
const inRealTimeMode = this.openmct.time.isRealTime();
return isSinglePlot && !isFrozen && !inRealTimeMode;
},

View File

@ -115,6 +115,10 @@ describe('the RemoteClock plugin', () => {
});
});
afterEach(() => {
openmct.time.setClock('local');
});
it('Does not throw error if time system is changed before remote clock initialized', () => {
expect(() => openmct.time.timeSystem('utc')).not.toThrow();
});

View File

@ -229,6 +229,7 @@ describe('the plugin', () => {
afterEach(() => {
openmct.router.path = originalRouterPath;
openmct.time.setClock('local');
});
it('Shows no progress bar initially', () => {

View File

@ -21,7 +21,8 @@
-->
<template>
<div
class="c-conductor"
ref="timeConductorOptionsHolder"
class="c-compact-tc is-expanded"
:class="[
{ 'is-zooming': isZooming },
{ 'is-panning': isPanning },
@ -29,75 +30,91 @@
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
]"
>
<div class="c-conductor__time-bounds">
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="viewBounds"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime v-else :input-bounds="viewBounds" @updated="saveClockOffsets" />
<ConductorModeIcon class="c-conductor__mode-icon" />
<conductor-axis
class="c-conductor__ticks"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
</div>
<div class="c-conductor__controls">
<ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" />
<ConductorHistory
class="c-conductor__history-select"
:offsets="openmct.time.clockOffsets()"
:bounds="bounds"
:time-system="timeSystem"
:mode="timeMode"
/>
<ConductorModeIcon class="c-conductor__mode-icon" />
<div class="c-compact-tc__setting-value u-fade-truncate">
<conductor-mode :mode="mode" :read-only="true" />
<conductor-clock :read-only="true" />
<conductor-time-system :read-only="true" />
</div>
<conductor-inputs-fixed v-if="isFixed" :input-bounds="viewBounds" :read-only="true" />
<conductor-inputs-realtime v-else :input-bounds="viewBounds" :read-only="true" />
<conductor-axis
v-if="isFixed"
class="c-conductor__ticks"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
<div class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear"></div>
<conductor-pop-up
v-if="showConductorPopup"
ref="conductorPopup"
:bottom="false"
:position-x="positionX"
:position-y="positionY"
:is-fixed="isFixed"
@popupLoaded="initializePopup"
@modeUpdated="saveMode"
@clockUpdated="saveClock"
@fixedBoundsUpdated="saveFixedBounds"
@clockOffsetsUpdated="saveClockOffsets"
@dismiss="clearPopup"
/>
</div>
</template>
<script>
import _ from 'lodash';
import ConductorMode from './ConductorMode.vue';
import ConductorTimeSystem from './ConductorTimeSystem.vue';
import {
FIXED_MODE_KEY,
MODES,
REALTIME_MODE_KEY,
TIME_CONTEXT_EVENTS
} from '../../api/time/constants';
import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorHistory from './ConductorHistory.vue';
import ConductorInputsFixed from './ConductorInputsFixed.vue';
import ConductorInputsRealtime from './ConductorInputsRealtime.vue';
import ConductorTimeSystem from './ConductorTimeSystem.vue';
import ConductorClock from './ConductorClock.vue';
import ConductorMode from './ConductorMode.vue';
import conductorPopUpManager from './conductorPopUpManager';
import ConductorPopUp from './ConductorPopUp.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
ConductorTimeSystem,
ConductorClock,
ConductorMode,
ConductorInputsRealtime,
ConductorInputsFixed,
ConductorMode,
ConductorTimeSystem,
ConductorAxis,
ConductorModeIcon,
ConductorHistory
ConductorPopUp
},
mixins: [conductorPopUpManager],
inject: ['openmct', 'configuration'],
data() {
let bounds = this.openmct.time.bounds();
let offsets = this.openmct.time.clockOffsets();
let timeSystem = this.openmct.time.timeSystem();
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let durationFormatter = this.getFormatter(
const isFixed = this.openmct.time.isFixed();
const bounds = this.openmct.time.getBounds();
const offsets = this.openmct.time.getClockOffsets();
const timeSystem = this.openmct.time.getTimeSystem();
const timeFormatter = this.getFormatter(timeSystem.timeFormat);
const durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
return {
timeSystem: timeSystem,
timeFormatter: timeFormatter,
durationFormatter: durationFormatter,
timeSystem,
timeFormatter,
durationFormatter,
offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end))
@ -114,37 +131,44 @@ export default {
start: bounds.start,
end: bounds.end
},
isFixed: this.openmct.time.clock() === undefined,
isFixed,
isUTCBased: timeSystem.isUTCBased,
showDatePicker: false,
showConductorPopup: false,
altPressed: false,
isPanning: false,
isZooming: false,
showTCInputStart: false,
showTCInputEnd: false
isZooming: false
};
},
computed: {
timeMode() {
return this.isFixed ? 'fixed' : 'realtime';
mode() {
return this.isFixed ? FIXED_MODE_KEY : REALTIME_MODE_KEY;
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('bounds', _.throttle(this.handleNewBounds, 300));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, _.throttle(this.handleNewBounds, 300));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, _.throttle(this.handleNewBounds, 300));
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
},
methods: {
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
handleNewBounds(bounds, isTick) {
if (this.openmct.time.isRealTime() || !isTick) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
}
},
setBounds(bounds) {
this.bounds = bounds;
@ -166,7 +190,7 @@ export default {
endPan(bounds) {
this.isPanning = false;
if (bounds) {
this.openmct.time.bounds(bounds);
this.openmct.time.setBounds(bounds);
}
},
zoom(bounds) {
@ -181,7 +205,7 @@ export default {
endZoom(bounds) {
this.isZooming = false;
if (bounds) {
this.openmct.time.bounds(bounds);
this.openmct.time.setBounds(bounds);
} else {
this.setViewFromBounds(this.bounds);
}
@ -194,9 +218,8 @@ export default {
);
this.isUTCBased = timeSystem.isUTCBased;
},
setViewFromClock(clock) {
// this.clearAllValidation();
this.isFixed = clock === undefined;
setMode() {
this.isFixed = this.openmct.time.isFixed();
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
@ -209,11 +232,24 @@ export default {
format: key
}).formatter;
},
saveClockOffsets(offsets) {
this.openmct.time.clockOffsets(offsets);
getBoundsForMode(mode) {
const isRealTime = mode === MODES.realtime;
return isRealTime ? this.openmct.time.getClockOffsets() : this.openmct.time.getBounds();
},
saveFixedOffsets(bounds) {
this.openmct.time.bounds(bounds);
saveFixedBounds(bounds) {
this.openmct.time.setBounds(bounds);
},
saveClockOffsets(offsets) {
this.openmct.time.setClockOffsets(offsets);
},
saveClock(clockOptions) {
this.openmct.time.setClock(clockOptions.clockKey);
},
saveMode(mode) {
this.openmct.time.setMode(mode, this.getBoundsForMode(mode));
},
copy(object) {
return JSON.parse(JSON.stringify(object));
}
}
};

View File

@ -30,6 +30,7 @@ import * as d3Selection from 'd3-selection';
import * as d3Axis from 'd3-axis';
import * as d3Scale from 'd3-scale';
import utcMultiTimeFormat from './utcMultiTimeFormat.js';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
const PADDING = 1;
const DEFAULT_DURATION_FORMATTER = 'duration';
@ -83,13 +84,16 @@ export default {
// draw x axis with labels. CSS is used to position them.
this.axisElement = vis.append('g').attr('class', 'axis');
this.setViewFromTimeSystem(this.openmct.time.timeSystem());
this.setViewFromTimeSystem(this.openmct.time.getTimeSystem());
this.setAxisDimensions();
this.setScale();
//Respond to changes in conductor
this.openmct.time.on('timeSystem', this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
beforeDestroy() {
clearInterval(this.resizeTimer);
},
methods: {
setAxisDimensions() {
@ -104,7 +108,7 @@ export default {
return;
}
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
if (timeSystem.isUTCBased) {
this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);
@ -140,7 +144,7 @@ export default {
this.setScale();
},
getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat);
@ -209,7 +213,7 @@ export default {
this.inPanMode = false;
},
getPanBounds() {
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const deltaTime = bounds.end - bounds.start;
const deltaX = this.dragX - this.dragStartX;
const percX = deltaX / this.width;
@ -272,7 +276,7 @@ export default {
};
},
scaleToBounds(value) {
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const timeDelta = bounds.end - bounds.start;
const valueDelta = value - this.left;
const offset = (valueDelta / this.width) * timeDelta;

View File

@ -0,0 +1,128 @@
/***************************************************************************** * Open MCT Web,
Copyright (c) 2014-2023, United States Government * as represented by the Administrator of the
National Aeronautics and Space * Administration. All rights reserved. * * Open MCT Web 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 Web 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 v-if="readOnly === false" ref="clockButton" class="c-tc-input-popup__options">
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu js-clock-button"
:class="[buttonCssClass, selectedClock.cssClass]"
@click.prevent.stop="showClocksMenu"
>
<span class="c-button__label">{{ selectedClock.name }}</span>
</button>
</div>
</div>
<div v-else class="c-compact-tc__setting-value__elem" :title="`Clock: ${selectedClock.name}`">
{{ selectedClock.name }}
</div>
</template>
<script>
import clockMixin from './clock-mixin';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
mixins: [clockMixin],
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: {
readOnly: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
const activeClock = this.getActiveClock();
return {
selectedClock: activeClock ? this.getClockMetadata(activeClock) : undefined,
clocks: []
};
},
mounted: function () {
this.loadClocks(this.configuration.menuOptions);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
methods: {
showClocksMenu() {
const elementBoundingClientRect = this.$refs.clockButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__clock-menu c-super-menu--sm',
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions);
},
setClock(clockKey) {
const option = {
clockKey
};
let configuration = this.getMatchingConfig({
clock: clockKey,
timeSystem: this.openmct.time.getTimeSystem().key
});
if (configuration === undefined) {
configuration = this.getMatchingConfig({
clock: clockKey
});
option.timeSystem = configuration.timeSystem;
option.bounds = configuration.bounds;
// this.openmct.time.setTimeSystem(configuration.timeSystem, configuration.bounds);
}
const offsets = this.openmct.time.getClockOffsets() ?? configuration.clockOffsets;
option.offsets = offsets;
this.$emit('clockUpdated', option);
},
getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock;
},
timeSystem(config) {
return options.timeSystem === config.timeSystem;
}
};
function configMatches(config) {
return Object.keys(options).reduce((match, option) => {
return match && matchers[option](config);
}, true);
}
return this.configuration.menuOptions.filter(configMatches)[0];
},
setViewFromClock(clock) {
this.selectedClock = this.getClockMetadata(clock);
}
}
};
</script>

View File

@ -25,6 +25,7 @@
<button
aria-label="Time Conductor History"
class="c-button--menu c-history-button icon-history"
:class="buttonCssClass"
@click.prevent.stop="showHistoryMenu"
>
<span class="c-button__label">History</span>
@ -41,29 +42,22 @@ const DEFAULT_RECORDS_LENGTH = 10;
import { millisecondsToDHMS } from 'utils/duration';
import UTCTimeFormat from '../utcTimeSystem/UTCTimeFormat.js';
import { REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
inject: ['openmct', 'configuration'],
props: {
bounds: {
type: Object,
required: true
},
offsets: {
type: Object,
required: false,
default: () => {}
},
timeSystem: {
type: Object,
required: true
},
mode: {
buttonCssClass: {
type: String,
required: true
required: false,
default() {
return '';
}
}
},
data() {
const mode = this.openmct.time.getMode();
return {
/**
* previous bounds entries available for easy re-use
@ -76,15 +70,15 @@ export default {
* @fixedHistory array of timespans
* @timespans {start, end} number representing timestamp
*/
mode,
currentHistory: mode + 'History',
fixedHistory: {},
presets: [],
isFixed: this.openmct.time.clock() === undefined
timeSystem: this.openmct.time.getTimeSystem(),
isFixed: this.openmct.time.isFixed()
};
},
computed: {
currentHistory() {
return this.mode + 'History';
},
historyForCurrentTimeSystem() {
const history = this[this.currentHistory][this.timeSystem.key];
@ -92,55 +86,29 @@ export default {
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (!this.isFixed) {
if (this.mode === REALTIME_MODE_KEY) {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
return key;
}
},
watch: {
bounds: {
handler() {
// only for fixed time since we track offsets for realtime
if (this.isFixed) {
this.updateMode();
this.addTimespan();
}
},
deep: true
},
offsets: {
handler() {
this.updateMode();
this.addTimespan();
},
deep: true
},
timeSystem: {
handler(ts) {
this.updateMode();
this.loadConfiguration();
this.addTimespan();
},
deep: true
},
mode: function () {
this.updateMode();
this.loadConfiguration();
}
},
mounted() {
this.updateMode();
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
this.loadConfiguration();
this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, this.addTimespan);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.addTimespan);
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.updateMode);
},
beforeDestroy() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.addTimespan);
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.addTimespan);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.modeChanged, this.updateMode);
},
methods: {
updateMode() {
this.isFixed = this.openmct.time.clock() === undefined;
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
},
getHistoryMenuItems() {
const descriptionDateFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
const history = this.historyForCurrentTimeSystem.map((timespan) => {
@ -151,7 +119,7 @@ export default {
descriptionDateFormat
)} - ${this.formatTime(timespan.end, descriptionDateFormat)}`;
if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) {
if (this.timeSystem.isUTCBased && !this.openmct.time.isRealTime()) {
name = `${startTime} ${millisecondsToDHMS(timespan.end - timespan.start)}`;
} else {
name = description;
@ -189,22 +157,41 @@ export default {
const localStorageHistory = localStorage.getItem(this.storageKey);
const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;
this[this.currentHistory] = history;
this.initializeHistoryIfNoHistory();
},
initializeHistoryIfNoHistory() {
if (!this[this.currentHistory]) {
this[this.currentHistory] = {};
this[this.currentHistory][this.timeSystem.key] = [];
this.persistHistoryToLocalStorage();
}
},
persistHistoryToLocalStorage() {
localStorage.setItem(this.storageKey, JSON.stringify(this[this.currentHistory]));
},
addTimespan() {
updateMode() {
this.mode = this.openmct.time.getMode();
this.currentHistory = this.mode + 'History';
this.loadConfiguration();
this.getHistoryFromLocalStorage();
},
updateTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.loadConfiguration();
this.getHistoryFromLocalStorage();
},
addTimespan(bounds, isTick) {
if (isTick) {
return;
}
const key = this.timeSystem.key;
let [...currentHistory] = this[this.currentHistory][key] || [];
const isFixed = this.openmct.time.isFixed();
let [...currentHistory] = this.historyForCurrentTimeSystem || [];
const timespan = {
start: this.isFixed ? this.bounds.start : this.offsets.start,
end: this.isFixed ? this.bounds.end : this.offsets.end
start: isFixed ? bounds.start : this.openmct.time.getClockOffsets().start,
end: isFixed ? bounds.end : this.openmct.time.getClockOffsets().end
};
// no dupes
@ -221,10 +208,10 @@ export default {
this.persistHistoryToLocalStorage();
},
selectTimespan(timespan) {
if (this.isFixed) {
this.openmct.time.bounds(timespan);
if (this.openmct.time.isFixed()) {
this.openmct.time.setBounds(timespan);
} else {
this.openmct.time.clockOffsets(timespan);
this.openmct.time.setClockOffsets(timespan);
}
},
selectPresetBounds(bounds) {
@ -262,7 +249,7 @@ export default {
let format = this.timeSystem.timeFormat;
let isNegativeOffset = false;
if (!this.isFixed) {
if (!this.openmct.time.isFixed()) {
if (time < 0) {
isNegativeOffset = true;
}

View File

@ -20,76 +20,42 @@
at runtime from the About dialog for additional information.
-->
<template>
<form ref="fixedDeltaInput" class="c-conductor__inputs">
<div class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed">
<!-- Fixed start -->
<div class="c-conductor__start-fixed__label">Start</div>
<input
ref="startDate"
v-model="formattedBounds.start"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="
validateAllBounds('startDate');
submitForm();
"
/>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
/>
<time-popup-fixed
v-if="readOnly === false"
:input-bounds="bounds"
:input-time-system="timeSystem"
@focus.native="$event.target.select()"
@update="setBoundsFromView"
@dismiss="dismiss"
/>
<div v-else class="c-compact-tc__setting-wrapper">
<div
class="c-compact-tc__setting-value u-fade-truncate--lg --no-sep"
:title="`Start bounds: ${formattedBounds.start}`"
>
{{ formattedBounds.start }}
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- Fixed end and RT 'last update' display -->
<div class="c-conductor__end-fixed__label">End</div>
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="
validateAllBounds('endDate');
submitForm();
"
/>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
/>
<div class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"></div>
<div
class="c-compact-tc__setting-value u-fade-truncate--lg --no-sep"
:title="`End bounds: ${formattedBounds.end}`"
>
{{ formattedBounds.end }}
</div>
</form>
</div>
</template>
<script>
import DatePicker from './DatePicker.vue';
import TimePopupFixed from './timePopupFixed.vue';
import _ from 'lodash';
const DEFAULT_DURATION_FORMATTER = 'duration';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
components: {
DatePicker
TimePopupFixed
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
inputBounds: {
type: Object,
default() {
@ -101,20 +67,27 @@ export default {
default() {
return [];
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
},
compact: {
type: Boolean,
default() {
return false;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
let durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
const timeSystem = this.openmct.time.getTimeSystem();
const timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.inputBounds || this.openmct.time.getBounds();
return {
showTCInputStart: true,
showTCInputEnd: true,
durationFormatter,
timeSystem,
timeFormatter,
bounds: {
start: bounds.start,
@ -128,8 +101,15 @@ export default {
};
},
watch: {
keyString() {
this.setTimeContext();
objectPath: {
handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
},
inputBounds: {
handler(newBounds) {
@ -140,41 +120,31 @@ export default {
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();
},
beforeDestroy() {
this.clearAllValidation();
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.handleNewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
this.handleNewBounds(this.timeContext.getBounds());
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
}
},
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
},
setBounds(bounds) {
this.bounds = bounds;
},
@ -185,9 +155,6 @@ export default {
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
this.isUTCBased = timeSystem.isUTCBased;
},
getFormatter(key) {
@ -195,112 +162,14 @@ export default {
format: key
}).formatter;
},
setBoundsFromView($event) {
if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse(this.formattedBounds.start);
let end = this.timeFormatter.parse(this.formattedBounds.end);
this.$emit('updated', {
start: start,
end: end
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
submitForm() {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.setBoundsFromView());
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = {
valid: true
};
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: 'Start and end difference exceeds allowable limit'
};
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
setBoundsFromView(bounds) {
this.$emit('boundsUpdated', {
start: bounds.start,
end: bounds.end
});
},
areBoundsFormatsValid() {
let validationResult = {
valid: true
};
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate =
input === this.$refs.startDate ? this.formattedBounds.start : this.formattedBounds.end;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = {
valid: false,
message: 'Invalid date'
};
}
return this.handleValidationResults(input, validationResult);
});
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter((option) => option.timeSystem === this.timeSystem.key)
.find((option) => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
this.$refs.fixedDeltaInput.reportValidity();
return validationResult.valid;
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date);
this.validateAllBounds('startDate');
this.submitForm();
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date);
this.validateAllBounds('endDate');
this.submitForm();
dismiss() {
this.$emit('dismissInputsFixed');
}
}
};

View File

@ -20,87 +20,53 @@
at runtime from the About dialog for additional information.
-->
<template>
<form ref="deltaInput" class="c-conductor__inputs">
<div class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta">
<!-- RT start -->
<div class="c-direction-indicator icon-minus"></div>
<time-popup
v-if="showTCInputStart"
class="pr-tc-input-menu--start"
:bottom="keyString !== undefined"
:type="'start'"
:offset="offsets.start"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="startOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset after now"
data-testid="conductor-start-offset-button"
@click.prevent.stop="showTimePopupStart"
>
{{ offsets.start }}
</button>
<time-popup-realtime
v-if="readOnly === false"
:offsets="offsets"
@focus.native="$event.target.select()"
@update="timePopUpdate"
@dismiss="dismiss"
/>
<div v-else class="c-compact-tc__setting-wrapper">
<div
v-if="!compact"
class="c-compact-tc__setting-value icon-minus u-fade-truncate--lg --no-sep"
:title="`Start offset: ${offsets.start}`"
>
{{ offsets.start }}
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- RT 'last update' display -->
<div class="c-conductor__end-fixed__label">Current</div>
<input
ref="endDate"
v-model="formattedCurrentValue"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
:disabled="true"
/>
<div v-if="!compact" class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"></div>
<div
v-if="!compact"
class="c-compact-tc__setting-value icon-plus u-fade-truncate--lg"
:class="{ '--no-sep': compact }"
:title="`End offset: ${offsets.end}`"
>
{{ offsets.end }}
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-delta">
<!-- RT end -->
<div class="c-direction-indicator icon-plus"></div>
<time-popup
v-if="showTCInputEnd"
class="pr-tc-input-menu--end"
:bottom="keyString !== undefined"
:type="'end'"
:offset="offsets.end"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="endOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset preceding now"
data-testid="conductor-end-offset-button"
@click.prevent.stop="showTimePopupEnd"
>
{{ offsets.end }}
</button>
<div
class="c-compact-tc__setting-value icon-clock c-compact-tc__current-update u-fade-truncate--lg --no-sep"
title="Last update"
>
{{ formattedCurrentValue }}
</div>
</form>
<div class="u-flex-spreader"></div>
</div>
</template>
<script>
import timePopup from './timePopup.vue';
import TimePopupRealtime from './timePopupRealtime.vue';
import _ from 'lodash';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
timePopup
TimePopupRealtime
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
objectPath: {
type: Array,
default() {
@ -112,17 +78,29 @@ export default {
default() {
return undefined;
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
},
compact: {
type: Boolean,
default() {
return false;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
let durationFormatter = this.getFormatter(
const timeSystem = this.openmct.time.getTimeSystem();
const durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
let offsets = this.openmct.time.clockOffsets();
let currentValue = this.openmct.time.clock()?.currentValue();
const timeFormatter = this.getFormatter(timeSystem.timeFormat);
const bounds = this.bounds ?? this.openmct.time.getBounds();
const offsets = this.offsets ?? this.openmct.time.getClockOffsets();
const currentValue = this.openmct.time.getClock()?.currentValue();
return {
showTCInputStart: false,
@ -147,8 +125,15 @@ export default {
};
},
watch: {
keyString() {
this.setTimeContext();
objectPath: {
handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
},
inputBounds: {
handler(newBounds) {
@ -159,45 +144,54 @@ export default {
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();
},
beforeDestroy() {
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.stopFollowingTime();
},
methods: {
followTime() {
this.handleNewBounds(this.timeContext.bounds());
this.setViewFromOffsets(this.timeContext.clockOffsets());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
this.timeContext.on('clockOffsets', this.setViewFromOffsets);
const bounds = this.timeContext
? this.timeContext.getBounds()
: this.openmct.time.getBounds();
const offsets = this.timeContext
? this.timeContext.getClockOffsets()
: this.openmct.time.getClockOffsets();
this.handleNewBounds(bounds);
this.setViewFromOffsets(offsets);
if (this.timeContext) {
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
} else {
this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
}
},
stopFollowingTime() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off('clockOffsets', this.setViewFromOffsets);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
} else {
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
}
},
setTimeContext() {
this.stopFollowingTime();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.followTime();
},
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
this.updateCurrentValue();
},
clearAllValidation() {
[this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
handleNewBounds(bounds, isTick) {
if (this.timeContext.isRealTime() || !isTick) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
this.updateCurrentValue();
}
},
setViewFromOffsets(offsets) {
if (offsets) {
@ -213,7 +207,7 @@ export default {
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
updateCurrentValue() {
const currentValue = this.openmct.time.clock()?.currentValue();
const currentValue = this.openmct.time.getClock()?.currentValue();
if (currentValue !== undefined) {
this.setCurrentValue(currentValue);
@ -236,85 +230,25 @@ export default {
format: key
}).formatter;
},
hideAllTimePopups() {
this.showTCInputStart = false;
this.showTCInputEnd = false;
},
showTimePopupStart() {
this.hideAllTimePopups();
this.showTCInputStart = !this.showTCInputStart;
},
showTimePopupEnd() {
this.hideAllTimePopups();
this.showTCInputEnd = !this.showTCInputEnd;
},
timePopUpdate({ type, hours, minutes, seconds }) {
this.offsets[type] = [hours, minutes, seconds].join(':');
timePopUpdate({ start, end }) {
this.offsets.start = [start.hours, start.minutes, start.seconds].join(':');
this.offsets.end = [end.hours, end.minutes, end.seconds].join(':');
this.setOffsetsFromView();
this.hideAllTimePopups();
},
setOffsetsFromView($event) {
if (this.$refs.deltaInput.checkValidity()) {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
setOffsetsFromView() {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
this.$emit('updated', {
start: startOffset,
end: endOffset
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = {
valid: true
};
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: 'Start and end difference exceeds allowable limit'
};
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
this.$emit('offsetsUpdated', {
start: startOffset,
end: endOffset
});
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
return validationResult.valid;
dismiss() {
this.$emit('dismissInputsRealtime');
},
copy(object) {
return JSON.parse(JSON.stringify(object));
}
}
};

View File

@ -20,42 +20,61 @@
at runtime from the About dialog for additional information.
-->
<template>
<div ref="modeButton" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<div v-if="readOnly === false" ref="modeButton" class="c-tc-input-popup__options">
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button class="c-button--menu c-mode-button" @click.prevent.stop="showModesMenu">
<button
class="c-button--menu js-mode-button"
:class="[buttonCssClass, selectedMode.cssClass]"
@click.prevent.stop="showModesMenu"
>
<span class="c-button__label">{{ selectedMode.name }}</span>
</button>
</div>
</div>
<div v-else class="c-compact-tc__setting-value__elem" :title="`Mode: ${selectedMode.name}`">
{{ selectedMode.name }}
</div>
</template>
<script>
import toggleMixin from '../../ui/mixins/toggle-mixin';
import modeMixin from './mode-mixin';
const TEST_IDS = true;
export default {
mixins: [toggleMixin],
mixins: [modeMixin],
inject: ['openmct', 'configuration'],
data: function () {
let activeClock = this.openmct.time.clock();
if (activeClock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
activeClock = Object.create(activeClock);
props: {
mode: {
type: String,
default() {
return undefined;
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
const mode = this.openmct.time.getMode();
return {
selectedMode: this.getModeOptionForClock(activeClock),
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
modes: [],
hoveredMode: {}
selectedMode: this.getModeMetadata(mode, TEST_IDS),
modes: []
};
},
mounted: function () {
this.loadClocksFromConfiguration();
this.openmct.time.on('clock', this.setViewFromClock);
watch: {
mode: {
handler(newMode) {
this.setViewFromMode(newMode);
}
}
},
destroyed: function () {
this.openmct.time.off('clock', this.setViewFromClock);
mounted: function () {
this.loadModes();
},
methods: {
showModesMenu() {
@ -64,112 +83,19 @@ export default {
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__mode-menu',
menuClass: 'c-conductor__mode-menu c-super-menu--sm',
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
},
loadClocksFromConfiguration() {
let clocks = this.configuration.menuOptions
.map((menuOption) => menuOption.clock)
.filter(isDefinedAndUnique)
.map(this.getClock);
/*
* Populate the modes menu with metadata from the available clocks
* "Fixed Mode" is always first, and has no defined clock
*/
this.modes = [undefined].concat(clocks).map(this.getModeOptionForClock);
function isDefinedAndUnique(key, index, array) {
return key !== undefined && array.indexOf(key) === index;
}
setViewFromMode(mode) {
this.selectedMode = this.getModeMetadata(mode, TEST_IDS);
},
setMode(mode) {
this.setViewFromMode(mode);
getModeOptionForClock(clock) {
if (clock === undefined) {
const key = 'fixed';
return {
key,
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
testId: 'conductor-modeOption-fixed',
onItemClicked: () => this.setOption(key)
};
} else {
const key = clock.key;
return {
key,
name: clock.name,
description:
'Monitor streaming data in real-time. The Time ' +
'Conductor and displays will automatically advance themselves based on this clock. ' +
clock.description,
cssClass: clock.cssClass || 'icon-clock',
testId: 'conductor-modeOption-realtime',
onItemClicked: () => this.setOption(key)
};
}
},
getClock(key) {
return this.openmct.time.getAllClocks().filter(function (clock) {
return clock.key === key;
})[0];
},
setOption(clockKey) {
if (clockKey === 'fixed') {
clockKey = undefined;
}
let configuration = this.getMatchingConfig({
clock: clockKey,
timeSystem: this.openmct.time.timeSystem().key
});
if (configuration === undefined) {
configuration = this.getMatchingConfig({
clock: clockKey
});
this.openmct.time.timeSystem(configuration.timeSystem, configuration.bounds);
}
if (clockKey === undefined) {
this.openmct.time.stopClock();
} else {
const offsets = this.openmct.time.clockOffsets() || configuration.clockOffsets;
this.openmct.time.clock(clockKey, offsets);
}
},
getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock;
},
timeSystem(config) {
return options.timeSystem === config.timeSystem;
}
};
function configMatches(config) {
return Object.keys(options).reduce((match, option) => {
return match && matchers[option](config);
}, true);
}
return this.configuration.menuOptions.filter(configMatches)[0];
},
setViewFromClock(clock) {
this.selectedMode = this.getModeOptionForClock(clock);
this.$emit('modeUpdated', mode);
}
}
};

View File

@ -21,6 +21,12 @@
-->
<template>
<div class="c-clock-symbol">
<svg class="c-clock-symbol__outer" viewBox="0 0 16 16">
<path d="M6 0L3 0C1.34315 0 0 1.34315 0 3V13C0 14.6569 1.34315 16 3 16H6V13H3V3H6V0Z" />
<path
d="M10 13H13V3H10V0H13C14.6569 0 16 1.34315 16 3V13C16 14.6569 14.6569 16 13 16H10V13Z"
/>
</svg>
<div class="hand-little"></div>
<div class="hand-big"></div>
</div>

View File

@ -0,0 +1,241 @@
<template>
<div class="c-tc-input-popup" :class="popupClasses" :style="position">
<div class="c-tc-input-popup__options">
<IndependentMode
v-if="isIndependent"
class="c-conductor__mode-select"
title="Sets the Time Conductor's mode."
:mode="timeOptionMode"
@independentModeUpdated="saveIndependentMode"
/>
<ConductorMode
v-else
class="c-conductor__mode-select"
title="Sets the Time Conductor's mode."
:button-css-class="'c-icon-button'"
@modeUpdated="saveMode"
/>
<IndependentClock
v-if="isIndependent"
class="c-conductor__mode-select"
title="Sets the Time Conductor's clock."
:clock="timeOptionClock"
:button-css-class="'c-icon-button'"
@independentClockUpdated="saveIndependentClock"
/>
<ConductorClock
v-else
class="c-conductor__mode-select"
title="Sets the Time Conductor's clock."
:button-css-class="'c-icon-button'"
@clockUpdated="saveClock"
/>
<!-- TODO: Time system and history must work even with ITC later -->
<ConductorTimeSystem
v-if="!isIndependent"
class="c-conductor__time-system-select"
title="Sets the Time Conductor's time system."
:button-css-class="'c-icon-button'"
/>
<ConductorHistory
v-if="!isIndependent"
class="c-conductor__history-select"
title="Select and apply previously entered time intervals."
:button-css-class="'c-icon-button'"
/>
</div>
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="bounds"
:object-path="objectPath"
@boundsUpdated="saveFixedBounds"
@dismissInputsFixed="dismiss"
/>
<conductor-inputs-realtime
v-else
:input-bounds="bounds"
:object-path="objectPath"
@offsetsUpdated="saveClockOffsets"
@dismissInputsRealtime="dismiss"
/>
</div>
</template>
<script>
import ConductorMode from './ConductorMode.vue';
import ConductorClock from './ConductorClock.vue';
import IndependentMode from './independent/IndependentMode.vue';
import IndependentClock from './independent/IndependentClock.vue';
import ConductorTimeSystem from './ConductorTimeSystem.vue';
import ConductorHistory from './ConductorHistory.vue';
import ConductorInputsFixed from './ConductorInputsFixed.vue';
import ConductorInputsRealtime from './ConductorInputsRealtime.vue';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
components: {
ConductorMode,
ConductorClock,
IndependentMode,
IndependentClock,
ConductorTimeSystem,
ConductorHistory,
ConductorInputsFixed,
ConductorInputsRealtime
},
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: {
positionX: {
type: Number,
required: true
},
positionY: {
type: Number,
required: true
},
isFixed: {
type: Boolean,
required: true
},
isIndependent: {
type: Boolean,
default() {
return false;
}
},
timeOptions: {
type: Object,
default() {
return undefined;
}
},
bottom: {
type: Boolean,
default() {
return false;
}
},
objectPath: {
type: Array,
default() {
return [];
}
}
},
data() {
const bounds = this.openmct.time.getBounds();
const timeSystem = this.openmct.time.getTimeSystem();
// const isFixed = this.openmct.time.isFixed();
return {
timeSystem,
bounds: {
start: bounds.start,
end: bounds.end
}
};
},
computed: {
position() {
const position = {
left: `${this.positionX}px`
};
if (this.isIndependent) {
position.top = `${this.positionY}px`;
}
return position;
},
popupClasses() {
const value = this.bottom ? 'c-tc-input-popup--bottom ' : '';
const mode = this.isFixed ? 'fixed-mode' : 'realtime-mode';
const independentClass = this.isIndependent ? 'itc-popout ' : '';
return `${independentClass}${value}c-tc-input-popup--${mode}`;
},
timeOptionMode() {
return this.timeOptions?.mode;
},
timeOptionClock() {
return this.timeOptions?.clock;
}
},
watch: {
objectPath: {
handler(newPath, oldPath) {
//domain object or view has probably changed
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
}
},
mounted() {
this.$emit('popupLoaded');
this.setTimeContext();
},
beforeDestroy() {
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
if (this.timeContext) {
this.stopFollowingTimeContext();
}
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.setBounds);
this.setViewFromClock(this.timeContext.getClock());
this.setBounds(this.timeContext.getBounds());
},
stopFollowingTimeContext() {
this.timeContext.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.setBounds);
},
setViewFromClock() {
this.bounds = this.isFixed
? this.timeContext.getBounds()
: this.openmct.time.getClockOffsets();
},
setBounds(bounds, isTick) {
if (this.isFixed || !isTick) {
this.bounds = bounds;
}
},
saveFixedBounds(bounds) {
this.$emit('fixedBoundsUpdated', bounds);
},
saveClockOffsets(offsets) {
this.$emit('clockOffsetsUpdated', offsets);
},
saveClock(clockOptions) {
this.$emit('clockUpdated', clockOptions);
},
saveMode(mode) {
this.$emit('modeUpdated', mode);
},
saveIndependentMode(mode) {
this.$emit('independentModeUpdated', mode);
},
saveIndependentClock(clockKey) {
this.$emit('independentClockUpdated', clockKey);
},
dismiss() {
this.$emit('dismiss');
}
}
};
</script>

View File

@ -21,38 +21,62 @@
-->
<template>
<div
v-if="selectedTimeSystem.name"
v-if="selectedTimeSystem.name && readOnly === false"
ref="timeSystemButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
<button
class="c-button--menu c-time-system-button"
:class="selectedTimeSystem.cssClass"
:class="[buttonCssClass]"
@click.prevent.stop="showTimeSystemMenu"
>
<span class="c-button__label">{{ selectedTimeSystem.name }}</span>
</button>
</div>
<div
v-else
class="c-compact-tc__setting-value__elem"
:title="`Time system: ${selectedTimeSystem.name}`"
>
{{ selectedTimeSystem.name }}
</div>
</template>
<script>
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
inject: ['openmct', 'configuration'],
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
let activeClock = this.openmct.time.clock();
let activeClock = this.openmct.time.getClock();
return {
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())),
timeSystems: this.getValidTimesystemsForClock(activeClock)
};
},
mounted: function () {
this.openmct.time.on('timeSystem', this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSysteChanged, this.setViewFromTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off('timeSystem', this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
methods: {
showTimeSystemMenu() {
@ -80,7 +104,7 @@ export default {
},
setTimeSystemFromView(timeSystem) {
if (timeSystem.key !== this.selectedTimeSystem.key) {
let activeClock = this.openmct.time.clock();
let activeClock = this.openmct.time.getClock();
let configuration = this.getMatchingConfig({
clock: activeClock && activeClock.key,
timeSystem: timeSystem.key
@ -89,15 +113,15 @@ export default {
let bounds;
if (this.selectedTimeSystem.isUTCBased && timeSystem.isUTCBased) {
bounds = this.openmct.time.bounds();
bounds = this.openmct.time.getBounds();
} else {
bounds = configuration.bounds;
}
this.openmct.time.timeSystem(timeSystem.key, bounds);
this.openmct.time.setTimeSystem(timeSystem.key, bounds);
} else {
this.openmct.time.timeSystem(timeSystem.key);
this.openmct.time.clockOffsets(configuration.clockOffsets);
this.openmct.time.setTimeSystem(timeSystem.key);
this.openmct.time.setClockOffsets(configuration.clockOffsets);
}
}
},
@ -126,7 +150,7 @@ export default {
},
setViewFromClock(clock) {
let activeClock = this.openmct.time.clock();
let activeClock = this.openmct.time.getClock();
this.timeSystems = this.getValidTimesystemsForClock(activeClock);
}
}

View File

@ -0,0 +1,52 @@
export default {
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
methods: {
loadClocks(menuOptions) {
let clocks;
if (menuOptions) {
clocks = menuOptions
.map((menuOption) => menuOption.clock)
.filter(isDefinedAndUnique)
.map(this.getClock);
} else {
clocks = this.openmct.time.getAllClocks();
}
this.clocks = clocks.map(this.getClockMetadata);
function isDefinedAndUnique(key, index, array) {
return key !== undefined && array.indexOf(key) === index;
}
},
getActiveClock() {
const activeClock = this.openmct.time.getClock();
//Create copy of active clock so the time API does not get reactified.
return Object.create(activeClock);
},
getClock(key) {
return this.openmct.time.getAllClocks().find((clock) => clock.key === key);
},
getClockMetadata(clock) {
const key = clock.key;
const clockOptions = {
key,
name: clock.name,
description: 'Uses the system clock as the current time basis. ' + clock.description,
cssClass: clock.cssClass || 'icon-clock',
onItemClicked: () => this.setClock(key)
};
return clockOptions;
}
}
};

View File

@ -57,11 +57,11 @@
}
.is-realtime-mode & {
$c: 1px solid rgba($colorTime, 0.7);
$c: 1px solid rgba($colorTimeRealtime, 0.7);
border-left: $c;
border-right: $c;
svg text {
fill: $colorTime;
fill: $colorTimeRealtime;
}
}
}

View File

@ -86,22 +86,17 @@
}
.c-clock-symbol {
$c: $colorBtnBg; //$colorObjHdrIc;
$d: 18px;
$c: rgba($colorBodyFg, 0.5);
$d: 16px;
height: $d;
width: $d;
position: relative;
&:before {
font-family: symbolsfont;
color: $c;
content: $glyph-icon-brackets;
font-size: $d;
line-height: normal;
display: block;
&__outer {
// SVG brackets shape
width: 100%;
height: 100%;
z-index: 1;
fill: $c;
}
// Clock hands
@ -117,6 +112,7 @@
left: 50%;
top: 50%;
z-index: 2;
&:before {
background: $c;
content: '';
@ -125,18 +121,22 @@
width: 100%;
bottom: -1px;
}
&.hand-little {
z-index: 2;
animation-duration: 12s;
transform: translate(-50%, -50%) rotate(120deg);
&:before {
height: ceil($handH * 0.6);
}
}
&.hand-big {
z-index: 1;
animation-duration: 1s;
transform: translate(-50%, -50%);
&:before {
height: $handH;
}
@ -146,14 +146,35 @@
// Modes
.is-realtime-mode &,
.is-lad-mode & {
&:before {
$c: $colorTimeRealtimeFgSubtle;
.c-clock-symbol__outer {
// Brackets icon
color: $colorTime;
fill: $c;
}
div[class*='hand'] {
animation-name: clock-hands;
&:before {
background: $colorTime;
background: $c;
}
}
}
}
// Contexts
.c-so-view--no-frame {
.c-compact-tc:not(.is-expanded) {
.c-clock-symbol {
$c: $frameControlsColorFg;
&__outer {
fill: $c;
}
div[class*='hand']:before {
background: $c;
}
}
}

View File

@ -1,14 +0,0 @@
.c-conductor__mode-menu {
max-height: 80vh;
max-width: 500px;
min-height: 250px;
z-index: 70;
[class*='__icon'] {
filter: $colorKeyFilter;
}
[class*='__item-description'] {
min-width: 200px;
}
}

View File

@ -1,56 +1,356 @@
.c-input--submit {
// Can't use display: none because some browsers will pretend the input doesn't exist, and enter won't work
visibility: none;
height: 0;
width: 0;
padding: 0;
// Can't use display: none because some browsers will pretend the input doesn't exist, and enter won't work
visibility: hidden;
height: 0;
width: 0;
padding: 0;
}
/*********************************************** CONDUCTOR LAYOUT */
.c-conductor {
&__inputs {
display: contents;
}
&__inputs {
display: flex;
flex: 0 0 auto;
&__time-bounds {
display: grid;
grid-column-gap: $interiorMargin;
grid-row-gap: $interiorMargin;
align-items: center;
> * + * {
margin-left: $interiorMargin;
}
}
// Default: fixed mode, desktop
grid-template-rows: 1fr;
grid-template-columns: 20px auto 1fr auto;
grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-end';
}
&__ticks {
flex: 1 1 auto;
}
&__mode-icon {
grid-area: tc-mode-icon;
}
&__controls {
grid-area: tc-controls;
display: flex;
align-items: center;
&__start-fixed,
&__start-delta {
grid-area: tc-start;
display: flex;
}
> * + * {
margin-left: $interiorMargin;
}
}
&__end-fixed,
&__end-delta {
grid-area: tc-end;
display: flex;
justify-content: flex-end;
}
/************************************ FIXED MODE STYLING */
&.is-fixed-mode {
.c-conductor-axis {
&__zoom-indicator {
border: 1px solid transparent;
display: none; // Hidden by default
}
}
&__ticks {
grid-area: tc-ticks;
}
&:not(.is-panning),
&:not(.is-zooming) {
.c-conductor-axis {
&:hover,
&:active {
cursor: col-resize;
}
}
}
&__controls {
grid-area: tc-controls;
&.is-panning,
&.is-zooming {
.c-conductor-input input {
// Styles for inputs while zooming or panning
background: rgba($timeConductorActiveBg, 0.4);
}
}
&.alt-pressed {
.c-conductor-axis:hover {
// When alt is being pressed and user is hovering over the axis, set the cursor
@include cursorGrab();
}
}
&.is-panning {
.c-conductor-axis {
@include cursorGrab();
background-color: $timeConductorActivePanBg;
transition: $transIn;
svg text {
stroke: $timeConductorActivePanBg;
transition: $transIn;
}
}
}
&.is-zooming {
.c-conductor-axis__zoom-indicator {
display: block;
position: absolute;
background: rgba($timeConductorActiveBg, 0.4);
border-left-color: $timeConductorActiveBg;
border-right-color: $timeConductorActiveBg;
top: 0;
bottom: 0;
}
}
}
/************************************ REAL-TIME MODE STYLING */
&.is-realtime-mode {
.c-conductor__time-bounds {
grid-template-columns: 20px auto 1fr auto auto;
grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-updated tc-end';
}
.c-conductor__end-fixed {
grid-area: tc-updated;
}
}
}
.c-conductor-holder--compact {
flex: 0 1 auto;
overflow: hidden;
.c-conductor {
&__inputs,
&__time-bounds {
display: flex;
flex: 0 1 auto;
overflow: hidden;
}
&__inputs {
> * + * {
margin-left: $interiorMarginSm;
}
}
}
.is-realtime-mode .c-conductor__end-fixed {
display: none !important;
}
}
.c-conductor-input {
color: $colorInputFg;
display: flex;
align-items: center;
justify-content: flex-start;
> * + * {
margin-left: $interiorMargin;
margin-left: $interiorMarginSm;
}
&:before {
// Realtime-mode clock icon symbol
margin-right: $interiorMarginSm;
}
input:invalid {
background: rgba($colorFormInvalid, 0.5);
}
}
.is-realtime-mode {
.c-conductor__delta-button {
color: $colorTimeRealtimeFg;
}
.c-conductor-input {
&:before {
color: $colorTimeRealtimeFgSubtle;
}
}
.c-conductor__end-fixed {
// Displays last RT update
color: $colorTimeRealtimeFgSubtle;
input {
// Remove input look
background: none;
box-shadow: none;
color: $colorTimeRealtimeFgSubtle;
pointer-events: none;
&[disabled] {
opacity: 1 !important;
}
}
}
}
.pr-tc-input-menu--start,
.pr-tc-input-menu--end {
background: $colorBodyBg;
border-radius: $controlCr;
display: grid;
grid-template-columns: 1fr 1fr 2fr;
grid-column-gap: 3px;
grid-row-gap: 4px;
align-items: start;
box-shadow: $shdwMenu;
padding: $interiorMarginLg;
position: absolute;
left: 8px;
bottom: 24px;
z-index: 99;
&[class*='--bottom'] {
bottom: auto;
top: 24px;
}
}
.pr-tc-input-menu {
&__options {
display: flex;
> * + * {
margin-left: $interiorMargin;
}
}
&__input-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
grid-column-gap: 3px;
grid-row-gap: $interiorMargin;
align-items: start;
}
}
.l-shell__time-conductor .pr-tc-input-menu--end {
left: auto;
right: 0;
}
.pr-time-label {
font-size: 0.9em;
text-transform: uppercase;
&:before {
font-size: 0.8em;
margin-right: $interiorMarginSm;
}
}
.pr-time-input {
display: flex;
align-items: center;
white-space: nowrap;
> * + * {
margin-left: $interiorMarginSm;
}
input {
height: 22px;
line-height: 1em;
font-size: 1.25em;
}
&--date input {
width: 85px;
}
&--time input {
width: 70px;
}
&--buttons {
> * + * {
margin-left: $interiorMargin;
}
}
&__start-end-sep {
height: 100%;
}
&--input-and-button {
@include wrappedInput();
padding-right: unset;
}
}
/*********************************************** COMPACT TIME CONDUCTOR */
.c-compact-tc,
.c-tc-input-popup {
[class*='start-end-sep'] {
opacity: 0.5;
}
}
.c-compact-tc {
border-radius: $controlCr;
display: flex;
flex: 0 1 auto;
align-items: center;
padding: 2px 0;
&__setting-wrapper {
display: contents;
}
&__setting-value {
border-right: 1px solid rgba($colorTimeCommonFg, 0.3);
cursor: pointer;
color: $colorTimeCommonFg;
align-items: center;
display: flex;
flex: 0 1 auto;
overflow: hidden;
padding: 0 $fadeTruncateW;
position: relative;
max-width: max-content;
text-transform: uppercase;
white-space: nowrap;
> * + * {
margin-left: $interiorMarginSm;
&:before {
content: " - ";
display: inline-block;
opacity: 0.4;
}
}
&[class*="icon"] {
&:before {
font-size: 0.75em;
line-height: 80%;
margin-right: $interiorMarginSm;
}
}
}
.c-toggle-switch,
.c-clock-symbol,
.c-conductor__mode-icon {
// Used in independent Time Conductor
flex: 0 0 auto;
}
.c-toggle-switch {
margin-right: $interiorMarginSm;
}
.c-conductor__mode-icon {
margin-left: $interiorMargin;
}
.c-so-view & {
// Time Conductor in a Layout frame
padding: 3px 0;
.c-clock-symbol {
$h: 13px;
height: $h;
width: $h;
}
[class*='button'] {
$p: 0px;
padding: $p $p + 2;
}
}
@ -112,199 +412,211 @@
}
}
}
}
&.is-realtime-mode {
.c-conductor__time-bounds {
grid-template-columns: 20px auto 1fr auto auto;
grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-updated tc-end';
}
.c-conductor__end-fixed {
grid-area: tc-updated;
}
}
body.phone.portrait & {
.c-conductor__time-bounds {
grid-row-gap: $interiorMargin;
grid-template-rows: auto auto;
grid-template-columns: 20px auto auto;
}
.c-conductor__controls {
padding-left: 25px; // Line up visually with other controls
}
&__mode-icon {
grid-row: 1;
}
&__ticks,
&__zoom {
display: none;
}
&.is-fixed-mode {
[class*='__start-fixed'],
[class*='__end-fixed'] {
[class*='__label'] {
// Start and end are in separate columns; make the labels line up
width: 30px;
.u-fade-truncate,
.u-fade-truncate--lg {
.is-fixed-mode & {
&:after {
@include fadeTruncate($color: $colorTimeFixedBg);
}
}
[class*='__end-input'] {
justify-content: flex-start;
}
.c-conductor__time-bounds {
grid-template-areas:
'tc-mode-icon tc-start tc-start'
'tc-mode-icon tc-end tc-end';
}
}
&.is-realtime-mode {
.c-conductor__time-bounds {
grid-template-areas:
'tc-mode-icon tc-start tc-updated'
'tc-mode-icon tc-end tc-end';
}
.c-conductor__end-fixed {
justify-content: flex-end;
}
.is-realtime-mode & {
&:after {
@include fadeTruncate($color: $colorTimeRealtimeBg);
}
}
}
}
.c-conductor-holder--compact {
min-height: 22px;
.itc-popout.c-tc-input-popup {
&--fixed-mode {
background: $colorTimeFixedBg;
color: $colorTimeFixedFgSubtle;
.c-conductor {
&__inputs,
&__time-bounds {
display: flex;
em,
.pr-time-label:before {
color: $colorTimeFixedFg;
}
.c-toggle-switch {
// Used in independent Time Conductor
flex: 0 0 auto;
}
&__bounds__valuelue {
color: $colorTimeFixedFg;
}
&__time-value {
color: $colorTimeFixedFg;
}
[class*='c-button--'] {
color: $colorTimeFixedBtnFg;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
}
.is-fixed-mode.is-expanded {
&.c-compact-tc,
.c-tc-input-popup {
background: $colorTimeFixedBg;
color: $colorTimeFixedFgSubtle;
em,
.pr-time-label:before {
color: $colorTimeFixedFg;
}
&__bounds__valuelue {
color: $colorTimeFixedFg;
}
&__time-value {
color: $colorTimeFixedFg;
}
[class*='c-button--'] {
color: $colorTimeFixedBtnFg;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
&__inputs {
> * + * {
margin-left: $interiorMarginSm;
}
&.c-compact-tc {
@include hover {
$c: $colorTimeFixedHov;
background: $c;
[class*='u-fade-truncate']:after {
@include fadeTruncate($color: $c);
}
}
}
}
.is-realtime-mode .c-conductor__end-fixed {
display: none !important;
}
}
.c-conductor-input {
color: $colorInputFg;
display: flex;
align-items: center;
justify-content: flex-start;
.itc-popout.c-tc-input-popup {
&--realtime-mode {
background: rgba($colorTimeRealtimeBg, 1);
color: $colorTimeRealtimeFgSubtle;
> * + * {
margin-left: $interiorMarginSm;
}
em,
.pr-time-label:before {
color: $colorTimeRealtimeFg;
}
&:before {
// Realtime-mode clock icon symbol
margin-right: $interiorMarginSm;
}
&__bounds__valuelue {
color: $colorTimeRealtimeFg;
}
.c-direction-indicator {
// Holds realtime-mode + and - symbols
font-size: 0.7em;
}
&__time-value {
color: $colorTimeRealtimeFg;
}
input:invalid {
background: rgba($colorFormInvalid, 0.5);
}
}
[class*='c-button--'] {
color: $colorTimeRealtimeBtnFg;
.is-realtime-mode {
.c-conductor__controls button,
.c-conductor__delta-button {
@include themedButton($colorTimeBg);
color: $colorTimeFg;
}
.c-conductor-input {
&:before {
color: $colorTime;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
}
}
.c-conductor__end-fixed {
// Displays last RT udpate
color: $colorTime;
.is-realtime-mode.is-expanded {
&.c-compact-tc,
.c-tc-input-popup {
background: rgba($colorTimeRealtimeBg, 1);
color: $colorTimeRealtimeFgSubtle;
input {
// Remove input look
background: none;
box-shadow: none;
color: $colorTime;
pointer-events: none;
em,
.pr-time-label:before {
color: $colorTimeRealtimeFg;
}
&[disabled] {
opacity: 1 !important;
}
&__bounds__valuelue {
color: $colorTimeRealtimeFg;
}
&__time-value {
color: $colorTimeRealtimeFg;
}
[class*='c-button--'] {
color: $colorTimeRealtimeBtnFg;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
}
}
[class^='pr-tc-input-menu'] {
// Uses ^= here to target both start and end menus
background: $colorBodyBg;
border-radius: $controlCr;
display: grid;
grid-template-columns: 1fr 1fr 2fr;
grid-column-gap: 3px;
grid-row-gap: 4px;
align-items: start;
box-shadow: $shdwMenu;
padding: $interiorMargin;
position: absolute;
left: 8px;
bottom: 24px;
z-index: 99;
&.c-compact-tc {
@include hover {
$c: $colorTimeRealtimeHov;
background: $c;
&[class*='--bottom'] {
bottom: auto;
top: 24px;
}
}
.l-shell__time-conductor .pr-tc-input-menu--end {
left: auto;
right: 0;
}
[class^='pr-time'] {
&[class*='label'] {
font-size: 0.8em;
opacity: 0.6;
text-transform: uppercase;
}
&[class*='controls'] {
display: flex;
align-items: center;
white-space: nowrap;
input {
height: 22px;
line-height: 22px;
margin-right: $interiorMarginSm;
font-size: 1.25em;
width: 42px;
[class*='u-fade-truncate']:after {
@include fadeTruncate($color: $c);
}
}
}
}
.c-compact-tc {
&.l-shell__time-conductor {
// Main view
min-height: 24px;
}
}
/*********************************************** INPUTS POPUP DIALOG */
.c-tc-input-popup {
@include menuOuter();
padding: $interiorMarginLg;
position: absolute;
width: min-content;
bottom: 35px;
> * + * {
margin-top: $interiorMarginLg;
}
&[class*='--bottom'] {
bottom: auto;
top: 35px;
}
&__options {
display: flex;
> * + * {
margin-left: $interiorMargin;
}
.c-button--menu {
padding: cButtonPadding($compact: true);
}
}
&--fixed-mode {
.c-tc-input-popup__input-grid {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 2fr;
}
}
&--realtime-mode {
.c-tc-input-popup__input-grid {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
}
}
&__input-grid {
display: grid;
grid-column-gap: 3px;
grid-row-gap: $interiorMargin;
align-items: start;
}
}
}

View File

@ -0,0 +1,88 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import raf from '@/utils/raf';
export default {
inject: ['openmct', 'configuration'],
data() {
return {
showConductorPopup: false,
positionX: 0,
positionY: 0,
conductorPopup: null
};
},
mounted() {
this.positionBox = raf(this.positionBox);
this.timeConductorOptionsHolder = this.$el;
this.timeConductorOptionsHolder.addEventListener('click', this.showPopup);
},
methods: {
initializePopup() {
this.conductorPopup = this.$refs.conductorPopup.$el;
this.$nextTick(() => {
window.addEventListener('resize', this.positionBox);
document.addEventListener('click', this.handleClickAway);
this.positionBox();
});
},
showPopup(clickEvent) {
const isAxis = clickEvent.target.closest('.c-conductor-axis') !== null;
if (isAxis || this.conductorPopup) {
return;
}
this.showConductorPopup = true;
},
positionBox() {
if (!this.conductorPopup) {
return;
}
const timeConductorOptionsBox = this.timeConductorOptionsHolder.getBoundingClientRect();
const offsetTop = this.conductorPopup.getBoundingClientRect().height;
this.positionY = timeConductorOptionsBox.top - offsetTop;
this.positionX = 0;
},
clearPopup() {
this.showConductorPopup = false;
this.conductorPopup = null;
document.removeEventListener('click', this.handleClickAway);
window.removeEventListener('resize', this.positionBox);
},
handleClickAway(clickAwayEvent) {
if (this.canClose(clickAwayEvent)) {
clickAwayEvent.stopPropagation();
this.clearPopup();
}
},
canClose(clickAwayEvent) {
const isChildMenu = clickAwayEvent.target.closest('.c-menu') !== null;
const isPopupElementItem = this.timeConductorOptionsHolder.contains(clickAwayEvent.target);
return !isChildMenu && !isPopupElementItem;
}
}
};

View File

@ -0,0 +1,122 @@
/***************************************************************************** * Open MCT Web,
Copyright (c) 2014-2023, United States Government * as represented by the Administrator of the
National Aeronautics and Space * Administration. All rights reserved. * * Open MCT Web 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 Web 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="clockMenuButton" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
v-if="selectedClock"
class="c-icon-button c-button--menu js-clock-button"
:class="[buttonCssClass, selectedClock.cssClass]"
@click.prevent.stop="showClocksMenu"
>
<span class="c-button__label">{{ selectedClock.name }}</span>
</button>
</div>
</div>
</template>
<script>
import toggleMixin from '../../../ui/mixins/toggle-mixin';
import clockMixin from '../clock-mixin';
import { TIME_CONTEXT_EVENTS } from '../../../api/time/constants';
export default {
mixins: [toggleMixin, clockMixin],
inject: ['openmct'],
props: {
clock: {
type: String,
default() {
return undefined;
}
},
enabled: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
const activeClock = this.getActiveClock();
return {
selectedClock: activeClock ? this.getClockMetadata(activeClock) : undefined,
clocks: []
};
},
watch: {
clock(newClock, oldClock) {
this.setViewFromClock(newClock);
},
enabled(newValue, oldValue) {
if (newValue !== undefined && newValue !== oldValue && newValue === true) {
this.setViewFromClock(this.clock);
}
}
},
beforeDestroy() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
mounted: function () {
this.loadClocks();
this.setViewFromClock(this.clock);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
methods: {
showClocksMenu() {
const elementBoundingClientRect = this.$refs.clockMenuButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
const menuOptions = {
menuClass: 'c-conductor__clock-menu c-super-menu--sm',
placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions);
},
getMenuOptions() {
let currentGlobalClock = this.getActiveClock();
//Create copy of active clock so the time API does not get reactified.
currentGlobalClock = Object.assign(
{},
{
name: currentGlobalClock.name,
clock: currentGlobalClock.key,
timeSystem: this.openmct.time.getTimeSystem().key
}
);
return [currentGlobalClock];
},
setClock(clockKey) {
this.setViewFromClock(clockKey);
this.$emit('independentClockUpdated', clockKey);
},
setViewFromClock(clockOrKey) {
let clock = clockOrKey;
if (!clock.key) {
clock = this.getClock(clockOrKey);
}
// if global clock changes, reload and pull it
this.selectedClock = this.getClockMetadata(clock);
}
}
};
</script>

View File

@ -0,0 +1,100 @@
<!--
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.
-->
<template>
<div ref="modeMenuButton" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-icon-button c-button--menu js-mode-button"
:class="[buttonCssClass, selectedMode.cssClass]"
@click.prevent.stop="showModesMenu"
>
<span class="c-button__label">{{ selectedMode.name }}</span>
</button>
</div>
</div>
</template>
<script>
import toggleMixin from '../../../ui/mixins/toggle-mixin';
import modeMixin from '../mode-mixin';
export default {
mixins: [toggleMixin, modeMixin],
inject: ['openmct'],
props: {
mode: {
type: String,
default() {
return undefined;
}
},
enabled: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
return {
selectedMode: this.getModeMetadata(this.mode),
modes: []
};
},
watch: {
mode: {
handler(newMode) {
this.setViewFromMode(newMode);
}
},
enabled(newValue, oldValue) {
if (newValue !== undefined && newValue !== oldValue && newValue === true) {
this.setViewFromMode(this.mode);
}
}
},
mounted: function () {
this.loadModes();
},
methods: {
showModesMenu() {
const elementBoundingClientRect = this.$refs.modeMenuButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
const menuOptions = {
menuClass: 'c-conductor__mode-menu c-super-menu--sm',
placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
},
setViewFromMode(mode) {
this.selectedMode = this.getModeMetadata(mode);
},
setMode(mode) {
this.setViewFromMode(mode);
this.$emit('independentModeUpdated', mode);
}
}
};
</script>

View File

@ -21,65 +21,88 @@
-->
<template>
<div
class="c-conductor"
ref="timeConductorOptionsHolder"
class="c-compact-tc"
:class="[
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode'
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode',
{ 'is-expanded': independentTCEnabled }
]"
>
<div class="c-conductor__time-bounds">
<toggle-switch
id="independentTCToggle"
:checked="independentTCEnabled"
:title="`${independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`"
@change="toggleIndependentTC"
/>
<toggle-switch
id="independentTCToggle"
class="c-toggle-switch--mini"
:checked="independentTCEnabled"
:title="toggleTitle"
@change="toggleIndependentTC"
/>
<ConductorModeIcon />
<ConductorModeIcon />
<div v-if="timeOptions && independentTCEnabled" class="c-conductor__controls">
<Mode
v-if="mode"
class="c-conductor__mode-select"
:key-string="domainObject.identifier.key"
:mode="timeOptions.mode"
:enabled="independentTCEnabled"
@modeChanged="saveMode"
/>
<conductor-inputs-fixed
v-if="showFixedInputs"
class="c-compact-tc__bounds--fixed"
:object-path="objectPath"
:read-only="true"
:compact="true"
/>
<conductor-inputs-fixed
v-if="isFixed"
:key-string="domainObject.identifier.key"
:object-path="objectPath"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime
v-if="showRealtimeInputs"
class="c-compact-tc__bounds--real-time"
:object-path="objectPath"
:read-only="true"
:compact="true"
/>
<div
v-if="independentTCEnabled"
class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear"
></div>
<conductor-inputs-realtime
v-else
:key-string="domainObject.identifier.key"
:object-path="objectPath"
@updated="saveClockOffsets"
/>
</div>
</div>
<conductor-pop-up
v-if="showConductorPopup"
ref="conductorPopup"
:object-path="objectPath"
:is-independent="true"
:time-options="timeOptions"
:is-fixed="isFixed"
:bottom="true"
:position-x="positionX"
:position-y="positionY"
@popupLoaded="initializePopup"
@independentModeUpdated="saveMode"
@independentClockUpdated="saveClock"
@fixedBoundsUpdated="saveFixedBounds"
@clockOffsetsUpdated="saveClockOffsets"
@dismiss="clearPopup"
/>
</div>
</template>
<script>
import { TIME_CONTEXT_EVENTS, FIXED_MODE_KEY } from '../../../api/time/constants';
import ConductorInputsFixed from '../ConductorInputsFixed.vue';
import ConductorInputsRealtime from '../ConductorInputsRealtime.vue';
import ConductorModeIcon from '@/plugins/timeConductor/ConductorModeIcon.vue';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import Mode from './Mode.vue';
import ConductorPopUp from '../ConductorPopUp.vue';
import independentTimeConductorPopUpManager from './independentTimeConductorPopUpManager';
export default {
components: {
Mode,
ConductorModeIcon,
ConductorInputsRealtime,
ConductorInputsFixed,
ConductorPopUp,
ToggleSwitch
},
inject: ['openmct'],
mixins: [independentTimeConductorPopUpManager],
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: {
domainObject: {
type: Object,
@ -91,22 +114,44 @@ export default {
}
},
data() {
const fixedOffsets = this.openmct.time.getBounds();
const clockOffsets = this.openmct.time.getClockOffsets();
const clock = this.openmct.time.getClock().key;
const mode = this.openmct.time.getMode();
const timeOptions = this.domainObject.configuration.timeOptions ?? {
clockOffsets,
fixedOffsets
};
timeOptions.clock = timeOptions.clock ?? clock;
timeOptions.mode = timeOptions.mode ?? mode;
// check for older configurations that stored a key
if (timeOptions.mode.key) {
timeOptions.mode = timeOptions.mode.key;
}
const isFixed = timeOptions.mode === FIXED_MODE_KEY;
return {
timeOptions: this.domainObject.configuration.timeOptions || {
clockOffsets: this.openmct.time.clockOffsets(),
fixedOffsets: this.openmct.time.bounds()
},
mode: undefined,
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true
timeOptions,
isFixed,
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true,
viewBounds: {
start: fixedOffsets.start,
end: fixedOffsets.end
}
};
},
computed: {
isFixed() {
if (!this.mode || !this.mode.key) {
return this.openmct.time.clock() === undefined;
} else {
return this.mode.key === 'fixed';
}
toggleTitle() {
return `${this.independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`;
},
showFixedInputs() {
return this.isFixed && this.independentTCEnabled;
},
showRealtimeInputs() {
return !this.isFixed && this.independentTCEnabled;
}
},
watch: {
@ -118,15 +163,33 @@ export default {
this.destroyIndependentTime();
this.independentTCEnabled = domainObject.configuration.useIndependentTime === true;
this.timeOptions = domainObject.configuration.timeOptions || {
clockOffsets: this.openmct.time.clockOffsets(),
fixedOffsets: this.openmct.time.bounds()
this.timeOptions = domainObject.configuration.timeOptions ?? {
clockOffsets: this.openmct.time.getClockOffsets(),
fixedOffsets: this.openmct.time.getBounds()
};
// these may not be set due to older configurations
this.timeOptions.clock = this.timeOptions.clock ?? this.openmct.time.getClock().key;
this.timeOptions.mode = this.timeOptions.mode ?? this.openmct.time.getMode();
// check for older configurations that stored a key
if (this.timeOptions.mode.key) {
this.timeOptions.mode = this.timeOptions.mode.key;
}
this.isFixed = this.timeOptions.mode === FIXED_MODE_KEY;
this.initialize();
}
},
deep: true
},
objectPath: {
handler(newPath, oldPath) {
//domain object or view has probably changed
this.setTimeContext();
},
deep: true
}
},
mounted() {
@ -141,120 +204,118 @@ export default {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.setTimeContext();
if (this.timeOptions.mode) {
this.mode = this.timeOptions.mode;
} else {
if (this.timeContext.clock() === undefined) {
this.timeOptions.mode = this.mode = { key: 'fixed' };
} else {
this.timeOptions.mode = this.mode = { key: Object.create(this.timeContext.clock()).key };
}
}
if (this.independentTCEnabled) {
this.registerIndependentTimeOffsets();
}
},
toggleIndependentTC() {
this.independentTCEnabled = !this.independentTCEnabled;
if (this.independentTCEnabled) {
this.registerIndependentTimeOffsets();
} else {
this.clearPopup();
this.destroyIndependentTime();
}
this.$emit('stateChanged', this.independentTCEnabled);
this.openmct.objects.mutate(
this.domainObject,
'configuration.useIndependentTime',
this.independentTCEnabled
);
},
setTimeContext() {
this.stopFollowingTimeContext();
if (this.timeContext) {
this.stopFollowingTimeContext();
}
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('clock', this.setTimeOptions);
this.timeContext.on(TIME_CONTEXT_EVENTS.clockChanged, this.setTimeOptionsClock);
this.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('clock', this.setTimeOptions);
}
this.timeContext.off(TIME_CONTEXT_EVENTS.clockChanged, this.setTimeOptionsClock);
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
},
setTimeOptions(clock) {
setTimeOptionsClock(clock) {
this.setTimeOptionsOffsets();
this.timeOptions.clock = clock.key;
},
setTimeOptionsMode(mode) {
this.setTimeOptionsOffsets();
this.timeOptions.mode = mode;
},
setTimeOptionsOffsets() {
this.timeOptions.clockOffsets =
this.timeOptions.clockOffsets || this.timeContext.clockOffsets();
this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets || this.timeContext.bounds();
if (!this.timeOptions.mode) {
this.mode =
this.timeContext.clock() === undefined
? { key: 'fixed' }
: { key: Object.create(this.timeContext.clock()).key };
this.registerIndependentTimeOffsets();
}
this.timeOptions.clockOffsets ?? this.timeContext.getClockOffsets();
this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets ?? this.timeContext.getBounds();
},
saveFixedOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
fixedOffsets: offsets
saveFixedBounds(bounds) {
const newOptions = this.updateTimeOptionProperty({
fixedOffsets: bounds
});
this.updateTimeOptions(newOptions);
},
saveClockOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
const newOptions = this.updateTimeOptionProperty({
clockOffsets: offsets
});
this.updateTimeOptions(newOptions);
},
saveMode(mode) {
this.mode = mode;
const newOptions = Object.assign({}, this.timeOptions, {
mode: this.mode
this.isFixed = mode === FIXED_MODE_KEY;
const newOptions = this.updateTimeOptionProperty({
mode: mode
});
this.updateTimeOptions(newOptions);
},
saveClock(clock) {
const newOptions = this.updateTimeOptionProperty({
clock
});
this.updateTimeOptions(newOptions);
},
updateTimeOptions(options) {
this.timeOptions = options;
if (!this.timeOptions.mode) {
this.timeOptions.mode = this.mode;
}
this.registerIndependentTimeOffsets();
this.$emit('updated', this.timeOptions);
this.$emit('updated', this.timeOptions); // no longer use this, but may be used elsewhere
this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', this.timeOptions);
},
registerIndependentTimeOffsets() {
if (!this.timeOptions.mode) {
return;
}
const timeContext = this.openmct.time.getIndependentContext(this.keyString);
let offsets;
if (this.isFixed) {
offsets = this.timeOptions.fixedOffsets;
offsets = this.timeOptions.fixedOffsets ?? this.timeContext.getBounds();
} else {
if (this.timeOptions.clockOffsets === undefined) {
this.timeOptions.clockOffsets = this.openmct.time.clockOffsets();
}
offsets = this.timeOptions.clockOffsets;
offsets = this.timeOptions.clockOffsets ?? this.openmct.time.getClockOffsets();
}
const timeContext = this.openmct.time.getIndependentContext(this.keyString);
if (!timeContext.hasOwnContext()) {
this.unregisterIndependentTime = this.openmct.time.addIndependentContext(
this.keyString,
offsets,
this.isFixed ? undefined : this.mode.key
this.isFixed ? undefined : this.timeOptions.clock
);
} else {
if (this.isFixed) {
timeContext.stopClock();
timeContext.bounds(offsets);
} else {
timeContext.clock(this.mode.key, offsets);
if (!this.isFixed) {
timeContext.setClock(this.timeOptions.clock);
}
timeContext.setMode(this.timeOptions.mode, offsets);
}
},
destroyIndependentTime() {
if (this.unregisterIndependentTime) {
this.unregisterIndependentTime();
}
},
updateTimeOptionProperty(option) {
return Object.assign({}, this.timeOptions, option);
}
}
};

View File

@ -1,231 +0,0 @@
<!--
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.
-->
<template>
<div v-if="modes.length > 1" ref="modeMenuButton" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
v-if="selectedMode"
class="c-button--menu c-mode-button"
@click.prevent.stop="showModesMenu"
>
<span class="c-button__label">{{ selectedMode.name }}</span>
</button>
</div>
</div>
</template>
<script>
import toggleMixin from '../../../ui/mixins/toggle-mixin';
export default {
mixins: [toggleMixin],
inject: ['openmct'],
props: {
mode: {
type: Object,
default() {
return undefined;
}
},
enabled: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
let clock;
if (this.mode && this.mode.key === 'fixed') {
clock = undefined;
} else {
//We want the clock from the global time context here
clock = this.openmct.time.clock();
}
if (clock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
clock = Object.create(clock);
}
return {
selectedMode: this.getModeOptionForClock(clock),
modes: []
};
},
watch: {
mode: {
deep: true,
handler(newMode) {
if (newMode) {
this.setViewFromClock(newMode.key === 'fixed' ? undefined : newMode);
}
}
},
enabled(newValue, oldValue) {
if (newValue !== undefined && newValue !== oldValue && newValue === true) {
this.setViewFromClock(this.mode.key === 'fixed' ? undefined : this.mode);
}
}
},
mounted: function () {
if (this.mode) {
this.setViewFromClock(this.mode.key === 'fixed' ? undefined : this.mode);
}
this.followTimeConductor();
},
destroyed: function () {
this.stopFollowTimeConductor();
},
methods: {
followTimeConductor() {
this.openmct.time.on('clock', this.setViewFromClock);
},
stopFollowTimeConductor() {
this.openmct.time.off('clock', this.setViewFromClock);
},
showModesMenu() {
const elementBoundingClientRect = this.$refs.modeMenuButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
const menuOptions = {
menuClass: 'c-conductor__mode-menu',
placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
},
getMenuOptions() {
let clocks = [
{
name: 'Fixed Timespan',
timeSystem: 'utc'
}
];
let currentGlobalClock = this.openmct.time.clock();
if (currentGlobalClock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
currentGlobalClock = Object.assign(
{},
{
name: currentGlobalClock.name,
clock: currentGlobalClock.key,
timeSystem: this.openmct.time.timeSystem().key
}
);
clocks.push(currentGlobalClock);
}
return clocks;
},
loadClocks() {
let clocks = this.getMenuOptions()
.map((menuOption) => menuOption.clock)
.filter(isDefinedAndUnique)
.map(this.getClock);
/*
* Populate the modes menu with metadata from the available clocks
* "Fixed Mode" is always first, and has no defined clock
*/
this.modes = [undefined].concat(clocks).map(this.getModeOptionForClock);
function isDefinedAndUnique(key, index, array) {
return key !== undefined && array.indexOf(key) === index;
}
},
getModeOptionForClock(clock) {
if (clock === undefined) {
const key = 'fixed';
return {
key,
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
onItemClicked: () => this.setOption(key)
};
} else {
const key = clock.key;
return {
key,
name: clock.name,
description:
'Monitor streaming data in real-time. The Time ' +
'Conductor and displays will automatically advance themselves based on this clock. ' +
clock.description,
cssClass: clock.cssClass || 'icon-clock',
onItemClicked: () => this.setOption(key)
};
}
},
getClock(key) {
return this.openmct.time.getAllClocks().filter(function (clock) {
return clock.key === key;
})[0];
},
setOption(clockKey) {
let key = clockKey;
if (clockKey === 'fixed') {
key = undefined;
}
const matchingOptions = this.getMenuOptions().filter((option) => option.clock === key);
const clock =
matchingOptions.length && matchingOptions[0].clock
? Object.assign({}, matchingOptions[0], { key: matchingOptions[0].clock })
: undefined;
this.selectedMode = this.getModeOptionForClock(clock);
if (this.mode) {
this.$emit('modeChanged', { key: clockKey });
}
},
setViewFromClock(clock) {
this.loadClocks();
//retain the mode chosen by the user
if (this.mode) {
let found = this.modes.find((mode) => mode.key === this.selectedMode.key);
if (!found) {
found = this.modes.find((mode) => mode.key === clock && clock.key);
this.setOption(
found ? this.getModeOptionForClock(clock).key : this.getModeOptionForClock().key
);
} else if (this.mode.key !== this.selectedMode.key) {
this.setOption(this.selectedMode.key);
}
} else {
this.setOption(this.getModeOptionForClock(clock).key);
}
}
}
};
</script>

View File

@ -0,0 +1,121 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import raf from '@/utils/raf';
import debounce from '@/utils/debounce';
export default {
data() {
return {
showConductorPopup: false,
positionX: -10000, // prevents initial flash after appending to body element
positionY: 0,
conductorPopup: null
};
},
mounted() {
this.positionBox = debounce(raf(this.positionBox), 250);
this.timeConductorOptionsHolder = this.$el;
this.timeConductorOptionsHolder.addEventListener('click', this.showPopup);
},
beforeDestroy() {
this.clearPopup();
},
methods: {
initializePopup() {
this.conductorPopup = this.$refs.conductorPopup.$el;
document.body.appendChild(this.conductorPopup); // remove from container as it (and it's ancestors) have overflow:hidden
this.$nextTick(() => {
window.addEventListener('resize', this.positionBox);
document.addEventListener('click', this.handleClickAway);
this.positionBox();
});
},
showPopup(clickEvent) {
const isToggle = clickEvent.target.classList.contains('c-toggle-switch__slider');
// no current popup,
// itc toggled,
// something is emitting a dupe event with pointer id = -1, want to ignore those
// itc is currently enabled
if (
!this.conductorPopup &&
!isToggle &&
clickEvent.pointerId !== -1 &&
this.independentTCEnabled
) {
this.showConductorPopup = true;
}
},
handleClickAway(clickEvent) {
if (this.canClose(clickEvent)) {
this.clearPopup();
}
},
positionBox() {
if (!this.conductorPopup) {
return;
}
const timeConductorOptionsBox = this.timeConductorOptionsHolder.getBoundingClientRect();
const topHalf = timeConductorOptionsBox.top < window.innerHeight / 2;
const padding = 5;
const offsetTop = this.conductorPopup.getBoundingClientRect().height;
const popupRight = timeConductorOptionsBox.left + this.conductorPopup.clientWidth;
const offsetLeft = Math.min(window.innerWidth - popupRight, 0);
if (topHalf) {
this.positionY =
timeConductorOptionsBox.bottom + this.conductorPopup.clientHeight + padding;
} else {
this.positionY = timeConductorOptionsBox.top - padding;
}
this.positionX = timeConductorOptionsBox.left + offsetLeft;
this.positionY = this.positionY - offsetTop;
},
clearPopup() {
if (!this.conductorPopup) {
return;
}
if (this.conductorPopup.parentNode === document.body) {
document.body.removeChild(this.conductorPopup);
}
this.showConductorPopup = false;
this.conductorPopup = null;
this.positionX = -10000; // reset it off screan
document.removeEventListener('click', this.handleClickAway);
window.removeEventListener('resize', this.positionBox);
},
canClose(clickAwayEvent) {
const isChildMenu = clickAwayEvent.target.closest('.c-menu') !== null;
const isPopupOrChild = clickAwayEvent.target.closest('.c-tc-input-popup') !== null;
const isTimeConductor = this.timeConductorOptionsHolder.contains(clickAwayEvent.target);
const isToggle = clickAwayEvent.target.classList.contains('c-toggle-switch__slider');
return !isTimeConductor && !isChildMenu && !isToggle && !isPopupOrChild;
}
}
};

View File

@ -0,0 +1,51 @@
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '../../api/time/constants';
export default {
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
methods: {
loadModes() {
this.modes = [FIXED_MODE_KEY, REALTIME_MODE_KEY].map(this.getModeMetadata);
},
getModeMetadata(mode, testIds = false) {
let modeOptions;
const key = mode;
if (key === FIXED_MODE_KEY) {
modeOptions = {
key,
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
onItemClicked: () => this.setMode(key)
};
if (testIds) {
modeOptions.testId = 'conductor-modeOption-fixed';
}
} else {
modeOptions = {
key,
name: 'Real-Time',
description:
'Monitor streaming data in real-time. The Time Conductor and displays will automatically advance themselves based on the active clock.',
cssClass: 'icon-clock',
onItemClicked: () => this.setMode(key)
};
if (testIds) {
modeOptions.testId = 'conductor-modeOption-realtime';
}
}
return modeOptions;
}
}
};

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import Conductor from './Conductor.vue';
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '../../api/time/constants';
function isTruthy(a) {
return Boolean(a);
@ -118,11 +119,34 @@ export default function (config) {
throwIfError(configResult);
const defaults = config.menuOptions[0];
if (defaults.clock) {
openmct.time.clock(defaults.clock, defaults.clockOffsets);
openmct.time.timeSystem(defaults.timeSystem, openmct.time.bounds());
const defaultClock = defaults.clock;
const defaultMode = defaultClock ? REALTIME_MODE_KEY : FIXED_MODE_KEY;
const defaultBounds = defaults?.bounds;
let clockOffsets = openmct.time.getClockOffsets();
if (defaultClock) {
openmct.time.setClock(defaults.clock);
clockOffsets = defaults.clockOffsets;
} else {
openmct.time.timeSystem(defaults.timeSystem, defaults.bounds);
// always have an active clock, regardless of mode
const firstClock = config.menuOptions.find((option) => option.clock);
if (firstClock) {
openmct.time.setClock(firstClock.clock);
clockOffsets = firstClock.clockOffsets;
}
}
openmct.time.setMode(defaultMode, defaultClock ? clockOffsets : defaultBounds);
openmct.time.setTimeSystem(defaults.timeSystem, defaultBounds);
//We are going to set the clockOffsets in fixed time mode since the conductor components down the line need these
if (clockOffsets && defaultMode === FIXED_MODE_KEY) {
openmct.time.setClockOffsets(clockOffsets);
}
//We are going to set the fixed time bounds in realtime time mode since the conductor components down the line need these
if (defaultBounds && defaultMode === REALTIME_MODE_KEY) {
openmct.time.setBounds(clockOffsets);
}
openmct.on('start', function () {

View File

@ -24,6 +24,7 @@ import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/te
import { millisecondsToDHMS, getPreciseDuration } from '../../utils/duration';
import ConductorPlugin from './plugin';
import Vue from 'vue';
import { FIXED_MODE_KEY } from '../../api/time/constants';
const THIRTY_SECONDS = 30 * 1000;
const ONE_MINUTE = THIRTY_SECONDS * 2;
@ -65,7 +66,6 @@ describe('time conductor', () => {
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(new ConductorPlugin(config));
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
@ -75,7 +75,7 @@ describe('time conductor', () => {
element.appendChild(child);
openmct.on('start', () => {
openmct.time.bounds({
openmct.time.setMode(FIXED_MODE_KEY, {
start: config.menuOptions[0].bounds.start,
end: config.menuOptions[0].bounds.end
});
@ -97,55 +97,63 @@ describe('time conductor', () => {
describe('in fixed time mode', () => {
it('shows delta inputs', () => {
const fixedModeEl = appHolder.querySelector('.is-fixed-mode');
const dateTimeInputs = fixedModeEl.querySelectorAll('.c-input--datetime');
expect(dateTimeInputs[0].value).toEqual('1978-01-19 23:30:00.000Z');
expect(dateTimeInputs[1].value).toEqual('1978-01-20 00:00:00.000Z');
expect(fixedModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual(
'Fixed Timespan'
);
const dateTimeInputs = fixedModeEl.querySelectorAll('.c-compact-tc__setting-value__elem');
expect(dateTimeInputs[0].innerHTML.trim()).toEqual('Fixed Timespan');
expect(dateTimeInputs[1].innerHTML.trim()).toEqual('Local Clock');
expect(dateTimeInputs[2].innerHTML.trim()).toEqual('UTC');
const dateTimes = fixedModeEl.querySelectorAll('.c-compact-tc__setting-value');
expect(dateTimes[1].innerHTML.trim()).toEqual('1978-01-19 23:30:00.000Z');
expect(dateTimes[2].innerHTML.trim()).toEqual('1978-01-20 00:00:00.000Z');
});
});
describe('in realtime mode', () => {
beforeEach((done) => {
const switcher = appHolder.querySelector('.c-mode-button');
const switcher = appHolder.querySelector('.is-fixed-mode');
const clickEvent = createMouseEvent('click');
switcher.dispatchEvent(clickEvent);
Vue.nextTick(() => {
const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1];
clockItem.dispatchEvent(clickEvent);
const modeButton = switcher.querySelector('.c-tc-input-popup .c-button--menu');
const clickEvent1 = createMouseEvent('click');
modeButton.dispatchEvent(clickEvent1);
Vue.nextTick(() => {
done();
const clockItem = document.querySelectorAll(
'.c-conductor__mode-menu .c-super-menu__menu li'
)[1];
const clickEvent2 = createMouseEvent('click');
clockItem.dispatchEvent(clickEvent2);
Vue.nextTick(() => {
done();
});
});
});
});
it('shows delta inputs', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button');
const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-compact-tc__setting-value__elem');
expect(dateTimeInputs[0].innerHTML.trim()).toEqual('Real-Time');
expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00');
expect(dateTimeInputs[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30');
const dateTimes = realtimeModeEl.querySelectorAll('.c-compact-tc__setting-value');
expect(dateTimes[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00');
expect(dateTimes[2].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30');
});
it('shows clock options', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
expect(realtimeModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual(
'Local Clock'
);
const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-compact-tc__setting-value__elem');
expect(dateTimeInputs[1].innerHTML.trim()).toEqual('Local Clock');
});
it('shows the current time', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
const currentTimeEl = realtimeModeEl.querySelector('.c-input--datetime');
const currentTimeEl = realtimeModeEl.querySelector('.c-compact-tc__current-update');
const currentTime = openmct.time.clock().currentValue();
const { start, end } = openmct.time.bounds();
expect(currentTime).toBeGreaterThan(start);
expect(currentTime).toBeLessThanOrEqual(end);
expect(currentTimeEl.value.length).toBeGreaterThan(0);
expect(currentTimeEl.innerHTML.trim().length).toBeGreaterThan(0);
});
});
});

View File

@ -1,198 +0,0 @@
<!--
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.
-->
<template>
<div
class="pr-tc-input-menu"
:class="{ 'pr-tc-input-menu--bottom': bottom === true }"
@keydown.enter.prevent
@keyup.enter.prevent="submit"
@keydown.esc.prevent
@keyup.esc.prevent="hide"
@click.stop
>
<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 class="pr-time-controls">
<input
ref="inputHrs"
v-model="inputHrs"
class="pr-time-controls__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputHrs')"
@wheel="increment($event, 'inputHrs')"
/>
:
</div>
<div class="pr-time-controls">
<input
ref="inputMins"
v-model="inputMins"
type="number"
class="pr-time-controls__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputMins')"
@wheel="increment($event, 'inputMins')"
/>
:
</div>
<div class="pr-time-controls">
<input
ref="inputSecs"
v-model="inputSecs"
type="number"
class="pr-time-controls__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputSecs')"
@wheel="increment($event, 'inputSecs')"
/>
<div class="pr-time__buttons">
<button
class="c-button c-button--major icon-check"
:disabled="isDisabled"
@click.prevent="submit"
></button>
<button class="c-button icon-x" @click.prevent="hide"></button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
required: true
},
offset: {
type: String,
required: true
},
bottom: {
type: Boolean,
default() {
return false;
}
}
},
data() {
return {
inputHrs: '00',
inputMins: '00',
inputSecs: '00',
isDisabled: false
};
},
mounted() {
this.setOffset();
document.addEventListener('click', this.hide);
},
beforeDestroy() {
document.removeEventListener('click', this.hide);
},
methods: {
format(ref) {
const curVal = this[ref];
this[ref] = curVal.padStart(2, '0');
},
validate() {
let disabled = false;
let refs = ['inputHrs', 'inputMins', 'inputSecs'];
for (let ref of refs) {
let min = Number(this.$refs[ref].min);
let max = Number(this.$refs[ref].max);
let value = Number(this.$refs[ref].value);
if (value > max || value < min) {
disabled = true;
break;
}
}
this.isDisabled = disabled;
},
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 step = ref === 'inputHrs' ? 1 : 5;
const maxVal = ref === 'inputHrs' ? 23 : 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(2, '0');
this.validate();
},
setOffset() {
[this.inputHrs, this.inputMins, this.inputSecs] = this.offset.split(':');
this.numberSelect('inputHrs');
},
numberSelect(input) {
this.$refs[input].focus();
// change to text, select, then change back to number
// number inputs do not support select()
this.$nextTick(() => {
this.$refs[input].setAttribute('type', 'text');
this.$refs[input].select();
this.$nextTick(() => {
this.$refs[input].setAttribute('type', 'number');
});
});
},
selectAll($ev) {
$ev.target.select();
}
}
};
</script>

View File

@ -0,0 +1,315 @@
<template>
<form ref="fixedDeltaInput" class="c-tc-input-popup__input-grid">
<div class="pr-time-label"><em>Start</em> Date</div>
<div class="pr-time-label">Time Z</div>
<div class="pr-time-label"></div>
<div class="pr-time-label"><em>End</em> Date</div>
<div class="pr-time-label">Time Z</div>
<div class="pr-time-label"></div>
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
<input
ref="startDate"
v-model="formattedBounds.start"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate')"
/>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
/>
</div>
<div class="pr-time-input pr-time-input--time">
<input
ref="startTime"
v-model="formattedBounds.startTime"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate')"
/>
</div>
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('endDate')"
/>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
/>
</div>
<div class="pr-time-input pr-time-input--time">
<input
ref="endTime"
v-model="formattedBounds.endTime"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('endDate')"
/>
</div>
<div class="pr-time-input pr-time-input--buttons">
<button
class="c-button c-button--major icon-check"
:disabled="isDisabled"
@click.prevent="submit"
></button>
<button class="c-button icon-x" @click.prevent="hide"></button>
</div>
</form>
</template>
<script>
import _ from 'lodash';
import DatePicker from './DatePicker.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
DatePicker
},
inject: ['openmct'],
props: {
inputBounds: {
type: Object,
required: true
},
inputTimeSystem: {
type: Object,
required: true
}
},
data() {
let timeSystem = this.openmct.time.getTimeSystem();
let durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.getBounds();
return {
timeFormatter,
durationFormatter,
bounds: {
start: bounds.start,
end: bounds.end
},
formattedBounds: {
start: timeFormatter.format(bounds.start).split(' ')[0],
end: timeFormatter.format(bounds.end).split(' ')[0],
startTime: durationFormatter.format(Math.abs(bounds.start)),
endTime: durationFormatter.format(Math.abs(bounds.end))
},
isUTCBased: timeSystem.isUTCBased,
isDisabled: false
};
},
watch: {
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
},
inputTimeSystem: {
handler(newTimeSystem) {
this.setTimeSystem(newTimeSystem);
},
deep: true
}
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())));
},
beforeDestroy() {
this.clearAllValidation();
},
methods: {
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
},
setBounds(bounds) {
this.bounds = bounds;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start).split(' ')[0];
this.formattedBounds.end = this.timeFormatter.format(bounds.end).split(' ')[0];
this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start));
this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end));
},
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
this.isUTCBased = timeSystem.isUTCBased;
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
setBoundsFromView(dismiss) {
if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse(
`${this.formattedBounds.start} ${this.formattedBounds.startTime}`
);
let end = this.timeFormatter.parse(
`${this.formattedBounds.end} ${this.formattedBounds.endTime}`
);
this.$emit('update', {
start: start,
end: end
});
}
if (dismiss) {
this.$emit('dismiss');
return false;
}
},
submit() {
this.validateAllBounds('startDate');
this.validateAllBounds('endDate');
this.submitForm(!this.isDisabled);
},
submitForm(dismiss) {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.setBoundsFromView(dismiss));
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = {
valid: true
};
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(
`${this.formattedBounds.start} ${this.formattedBounds.startTime}`
),
end: this.timeFormatter.parse(
`${this.formattedBounds.end} ${this.formattedBounds.endTime}`
)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: 'Start and end difference exceeds allowable limit'
};
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
});
},
areBoundsFormatsValid() {
let validationResult = {
valid: true
};
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate =
input === this.$refs.startDate
? `${this.formattedBounds.start} ${this.formattedBounds.startTime}`
: `${this.formattedBounds.end} ${this.formattedBounds.endTime}`;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = {
valid: false,
message: 'Invalid date'
};
}
return this.handleValidationResults(input, validationResult);
});
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter((option) => option.timeSystem === this.timeSystem.key)
.find((option) => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
this.isDisabled = true;
} else {
input.setCustomValidity('');
input.title = '';
this.isDisabled = false;
}
this.$refs.fixedDeltaInput.reportValidity();
return validationResult.valid;
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date).split(' ')[0];
this.validateAllBounds('startDate');
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date).split(' ')[0];
this.validateAllBounds('endDate');
},
hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) {
this.$emit('dismiss');
}
}
}
};
</script>

View File

@ -0,0 +1,254 @@
<template>
<form ref="deltaInput" class="c-tc-input-popup__input-grid">
<div class="pr-time-label icon-minus">Hrs</div>
<div class="pr-time-label">Mins</div>
<div class="pr-time-label">Secs</div>
<div class="pr-time-label"></div>
<div class="pr-time-label icon-plus">Hrs</div>
<div class="pr-time-label">Mins</div>
<div class="pr-time-label">Secs</div>
<div class="pr-time-label"></div>
<div class="pr-time-input">
<input
ref="startInputHrs"
v-model="startInputHrs"
class="pr-time-input__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('startInputHrs')"
@wheel="increment($event, 'startInputHrs')"
/>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="startInputMins"
v-model="startInputMins"
type="number"
class="pr-time-input__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('startInputMins')"
@wheel="increment($event, 'startInputMins')"
/>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="startInputSecs"
v-model="startInputSecs"
type="number"
class="pr-time-input__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('startInputSecs')"
@wheel="increment($event, 'startInputSecs')"
/>
</div>
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
<div class="pr-time-input">
<input
ref="endInputHrs"
v-model="endInputHrs"
class="pr-time-input__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('endInputHrs')"
@wheel="increment($event, 'endInputHrs')"
/>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="endInputMins"
v-model="endInputMins"
type="number"
class="pr-time-input__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('endInputMins')"
@wheel="increment($event, 'endInputMins')"
/>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="endInputSecs"
v-model="endInputSecs"
type="number"
class="pr-time-input__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('endInputSecs')"
@wheel="increment($event, 'endInputSecs')"
/>
</div>
<div class="pr-time-input pr-time-input--buttons">
<button
class="c-button c-button--major icon-check"
:disabled="isDisabled"
@click.prevent="submit"
></button>
<button class="c-button icon-x" @click.prevent="hide"></button>
</div>
</form>
</template>
<script>
export default {
props: {
offsets: {
type: Object,
required: true
}
},
data() {
return {
startInputHrs: '00',
startInputMins: '00',
startInputSecs: '00',
endInputHrs: '00',
endInputMins: '00',
endInputSecs: '00',
isDisabled: false
};
},
watch: {
offsets: {
handler() {
this.setOffsets();
},
deep: true
}
},
mounted() {
this.setOffsets();
document.addEventListener('click', this.hide);
},
beforeDestroy() {
document.removeEventListener('click', this.hide);
},
methods: {
format(ref) {
const curVal = this[ref];
this[ref] = curVal.padStart(2, '0');
},
validate() {
let disabled = false;
let refs = [
'startInputHrs',
'startInputMins',
'startInputSecs',
'endInputHrs',
'endInputMins',
'endInputSecs'
];
for (let ref of refs) {
let min = Number(this.$refs[ref].min);
let max = Number(this.$refs[ref].max);
let value = Number(this.$refs[ref].value);
if (value > max || value < min) {
disabled = true;
break;
}
}
this.isDisabled = disabled;
},
submit() {
this.$emit('update', {
start: {
hours: this.startInputHrs,
minutes: this.startInputMins,
seconds: this.startInputSecs
},
end: {
hours: this.endInputHrs,
minutes: this.endInputMins,
seconds: this.endInputSecs
}
});
this.$emit('dismiss');
},
hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) {
this.$emit('dismiss');
}
},
increment($ev, ref) {
$ev.preventDefault();
const step = ref === 'startInputHrs' || ref === 'endInputHrs' ? 1 : 5;
const maxVal = ref === 'startInputHrs' || ref === 'endInputHrs' ? 23 : 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(2, '0');
this.validate();
},
setOffsets() {
[this.startInputHrs, this.startInputMins, this.startInputSecs] =
this.offsets.start.split(':');
[this.endInputHrs, this.endInputMins, this.endInputSecs] = this.offsets.end.split(':');
this.numberSelect('startInputHrs');
},
numberSelect(input) {
this.$refs[input].focus();
// change to text, select, then change back to number
// number inputs do not support select()
this.$nextTick(() => {
if (this.$refs[input] === undefined) {
return;
}
this.$refs[input].setAttribute('type', 'text');
this.$refs[input].select();
this.$nextTick(() => {
this.$refs[input].setAttribute('type', 'number');
});
});
},
selectAll($ev) {
$ev.target.select();
}
}
};
</script>

View File

@ -25,6 +25,7 @@ import TimelistPlugin from './plugin';
import { TIMELIST_TYPE } from './constants';
import Vue from 'vue';
import EventEmitter from 'EventEmitter';
import { FIXED_MODE_KEY } from '../../api/time/constants';
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
@ -32,7 +33,7 @@ const LIST_ITEM_CLASS = '.js-table__body .js-list-item';
const LIST_ITEM_VALUE_CLASS = '.js-list-item__value';
const LIST_ITEM_BODY_CLASS = '.js-table__body th';
describe('the plugin', function () {
xdescribe('the plugin', function () {
let timelistDefinition;
let element;
let child;
@ -87,6 +88,10 @@ describe('the plugin', function () {
end: twoHoursFuture
}
});
openmct.time.setMode(FIXED_MODE_KEY, {
start: twoHoursPast,
end: twoHoursFuture
});
openmct.install(new TimelistPlugin());
timelistDefinition = openmct.types.get(TIMELIST_TYPE).definition;
@ -399,7 +404,7 @@ describe('the plugin', function () {
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(2);
expect(items.length).toEqual(1);
});
});
});

View File

@ -39,7 +39,7 @@ export default class PauseTimerAction {
const newConfiguration = { ...domainObject.configuration };
newConfiguration.timerState = 'paused';
newConfiguration.pausedTime = new Date();
newConfiguration.pausedTime = new Date(this.openmct.time.now());
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
}

View File

@ -39,7 +39,7 @@ export default class RestartTimerAction {
const newConfiguration = { ...domainObject.configuration };
newConfiguration.timerState = 'started';
newConfiguration.timestamp = new Date();
newConfiguration.timestamp = new Date(this.openmct.time.now());
newConfiguration.pausedTime = undefined;
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);

View File

@ -50,7 +50,7 @@ export default class StartTimerAction {
timestamp = moment(timestamp);
}
const now = moment(new Date());
const now = moment(new Date(this.openmct.time.now()));
if (pausedTime) {
const timeShift = moment.duration(now.diff(pausedTime));
const shiftedTime = timestamp.add(timeShift);

View File

@ -42,7 +42,7 @@
</template>
<script>
import ticker from 'utils/clock/Ticker';
import raf from 'utils/raf';
const moment = require('moment-timezone');
const momentDurationFormatSetup = require('moment-duration-format');
@ -59,8 +59,7 @@ export default {
},
data() {
return {
lastTimestamp: undefined,
active: true
lastTimestamp: undefined
};
},
computed: {
@ -184,15 +183,13 @@ export default {
},
mounted() {
this.$nextTick(() => {
if (this.configuration && this.configuration.timerState === undefined) {
if (!this.configuration?.timerState) {
const timerAction = !this.relativeTimestamp ? 'stop' : 'start';
this.triggerAction(`timer.${timerAction}`);
}
window.requestAnimationFrame(this.tick);
this.unlisten = ticker.listen(() => {
this.openmct.objects.refresh(this.domainObject);
});
this.handleTick = raf(this.handleTick);
this.openmct.time.on('tick', this.handleTick);
this.viewActionsCollection = this.openmct.actions.getActionsCollection(
this.objectPath,
@ -202,25 +199,21 @@ export default {
});
},
beforeDestroy() {
this.active = false;
if (this.unlisten) {
this.unlisten();
}
this.openmct.time.off('tick', this.handleTick);
},
methods: {
tick() {
handleTick() {
const isTimerRunning = !['paused', 'stopped'].includes(this.timerState);
if (isTimerRunning) {
this.lastTimestamp = new Date();
this.lastTimestamp = new Date(this.openmct.time.now());
}
if (this.timerState === 'paused' && !this.lastTimestamp) {
this.lastTimestamp = this.pausedTime;
}
if (this.active) {
window.requestAnimationFrame(this.tick);
}
this.openmct.objects.refresh(this.domainObject);
},
restartTimer() {
this.triggerAction('timer.restart');

View File

@ -25,7 +25,7 @@ import timerPlugin from './plugin';
import Vue from 'vue';
describe('Timer plugin:', () => {
xdescribe('Timer plugin:', () => {
let openmct;
let timerDefinition;
let element;

View File

@ -43,6 +43,7 @@ export default class LocalClock extends DefaultClock {
}
start() {
super.tick(this.lastTick);
this.timeoutHandle = setTimeout(this.tick.bind(this), this.period);
}

View File

@ -83,6 +83,7 @@ $colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #ccc;
$colorAHov: #fff;
$filterHov: brightness(1.3) contrast(1.5); // Tree, location items
$filterHovSubtle: brightness(1.2) contrast(1.2);
$colorSelectedBg: rgba($colorKey, 0.3);
$colorSelectedFg: pullForward($colorBodyFg, 20%);
@ -144,13 +145,31 @@ $colorBodyBgSubtleHov: pullForward($colorBodyBg, 10%);
$colorKeySubtle: pushBack($colorKey, 10%);
// Time Colors
$colorTime: #618cff;
$colorTimeBg: $colorTime;
$colorTimeFg: pullForward($colorTimeBg, 30%);
$colorTimeHov: pullForward($colorTime, 10%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTimeCommonFg: #eee;
$colorTimeFixed: #59554c;
$colorTimeFixedBg: $colorTimeFixed;
$colorTimeFixedFg: #eee;
$colorTimeFixedFgSubtle: #b2aa98;
$colorTimeFixedHov: pullForward($colorTimeFixed, 5%);
$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);
$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);
$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;
$colorTimeFixedBtnBgMajor: #a09375;
$colorTimeFixedBtnFgMajor: #fff;
$colorTimeRealtime: #445890;
$colorTimeRealtimeBg: $colorTimeRealtime;
$colorTimeRealtimeFg: #eee;
$colorTimeRealtimeFgSubtle: #88b0ff;
$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 5%);
$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);
$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);
$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;
$colorTimeRealtimeBtnBgMajor: #588ffa;
$colorTimeRealtimeBtnFgMajor: #fff;
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(1.2);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #226074;

View File

@ -87,6 +87,7 @@ $colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #ccc;
$colorAHov: #fff;
$filterHov: brightness(1.3) contrast(1.5); // Tree, location items
$filterHovSubtle: brightness(1.2) contrast(1.2);
$colorSelectedBg: rgba($colorKey, 0.3);
$colorSelectedFg: pullForward($colorBodyFg, 20%);
@ -148,13 +149,31 @@ $colorBodyBgSubtleHov: pushBack($colorKey, 50%);
$colorKeySubtle: pushBack($colorKey, 10%);
// Time Colors
$colorTime: #618cff;
$colorTimeBg: $colorTime;
$colorTimeFg: pullForward($colorTimeBg, 30%);
$colorTimeHov: pullForward($colorTime, 10%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTimeCommonFg: #eee;
$colorTimeFixed: #59554c;
$colorTimeFixedBg: $colorTimeFixed;
$colorTimeFixedFg: #eee;
$colorTimeFixedFgSubtle: #b2aa98;
$colorTimeFixedHov: pullForward($colorTimeFixed, 10%);
$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);
$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);
$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;
$colorTimeFixedBtnBgMajor: #a09375;
$colorTimeFixedBtnFgMajor: #fff;
$colorTimeRealtime: #445890;
$colorTimeRealtimeBg: $colorTimeRealtime;
$colorTimeRealtimeFg: #eee;
$colorTimeRealtimeFgSubtle: #88b0ff;
$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 10%);
$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);
$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);
$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;
$colorTimeRealtimeBtnBgMajor: #588ffa;
$colorTimeRealtimeBtnFgMajor: #fff;
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(1.2);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #226074;

View File

@ -83,6 +83,7 @@ $colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: $colorBodyFg;
$colorAHov: $colorKey;
$filterHov: hue-rotate(-10deg) brightness(0.8) contrast(2); // Tree, location items
$filterHovSubtle: hue-rotate(-8deg) brightness(0.5) contrast(1.2);
$colorSelectedBg: pushBack($colorKey, 40%);
$colorSelectedFg: pullForward($colorBodyFg, 10%);
@ -144,13 +145,31 @@ $colorBodyBgSubtleHov: pullForward($colorBodyBg, 10%);
$colorKeySubtle: pushBack($colorKey, 20%);
// Time Colors
$colorTime: #618cff;
$colorTimeBg: $colorTime;
$colorTimeFg: $colorBodyBg;
$colorTimeHov: pushBack($colorTime, 5%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTimeCommonFg: #eee;
$colorTimeFixed: #59554c;
$colorTimeFixedBg: $colorTimeFixed;
$colorTimeFixedFg: #eee;
$colorTimeFixedFgSubtle: #b2aa98;
$colorTimeFixedHov: pullForward($colorTimeFixed, 10%);
$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);
$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);
$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;
$colorTimeFixedBtnBgMajor: #a09375;
$colorTimeFixedBtnFgMajor: #fff;
$colorTimeRealtime: #445890;
$colorTimeRealtimeBg: $colorTimeRealtime;
$colorTimeRealtimeFg: #eee;
$colorTimeRealtimeFgSubtle: #88b0ff;
$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 10%);
$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);
$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);
$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;
$colorTimeRealtimeBtnBgMajor: #588ffa;
$colorTimeRealtimeBtnFgMajor: #fff;
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(0.8);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #a0cde1;

View File

@ -50,6 +50,7 @@ $treeNavArrowD: 20px;
$shellMainBrowseBarH: 22px;
$shellTimeConductorH: 55px;
$shellToolBarH: 29px;
$fadeTruncateW: 7px;
/*************** Items */
$itemPadLR: 5px;
$gridItemDesk: 175px;

View File

@ -244,6 +244,12 @@ button {
}
}
.c-not-button {
// Use within a holder that's clickable; use to indicate interactability
@include cButtonLayout();
cursor: pointer;
}
/******************************************************** DISCLOSURE CONTROLS */
/********* Disclosure Button */
// Provides a downward arrow icon that when clicked displays additional options and/or info.
@ -631,6 +637,7 @@ select {
.c-super-menu {
// Two column layout, menu items on left with detail of hover element on right
$m: $interiorMarginLg;
@include menuOuter();
@include menuPositioning();
display: flex;
@ -638,20 +645,21 @@ select {
flex-direction: row;
> [class*='__'] {
$m: $interiorMarginLg;
flex: 1 1 50%;
&:first-child {
margin-right: $m;
}
//flex: 1 1 50%;
//&:first-child {
// margin-right: $m;
//}
&:last-child {
border-left: 1px solid $colorInteriorBorder;
padding-left: $m;
//border-left: 1px solid $colorInteriorBorder;
//padding-left: $m;
}
}
&__menu {
@include menuInner();
flex: 1 1 50%;
margin-right: $m;
overflow: auto;
ul {
@ -664,16 +672,18 @@ select {
}
&__item-description {
border-left: 1px solid $colorInteriorBorder;
flex: 1 1 50%;
padding-left: $m;
display: flex;
flex-direction: column;
justify-content: stretch;
.l-item-description {
&__name,
&__description {
margin-top: $interiorMarginLg;
}
> * + * {
margin-top: $interiorMarginLg;
}
.l-item-description {
&__icon {
min-height: 20%;
margin: 10% 25%;
@ -691,6 +701,33 @@ select {
}
}
}
.c-super-menu--sm {
// Small version of the super menu, used by the compact Time Conductor
height: 120px;
width: 500px;
.c-super-menu__menu {
flex: 1 1 30%;
}
.c-super-menu__item-description {
flex: 1 1 70%;
[class*="__icon"] {
display: none !important;
}
[class*="__name"] {
margin-top: 0 !important;
}
[class*="__item-description"] {
min-width: 200px;
}
}
}
/******************************************************** CONTROL BARS */
.c-control-bar {
display: flex;

View File

@ -63,6 +63,11 @@ div {
}
}
.u-flex-spreader {
// Pushes against elements in a flex layout to spread them out
flex: 1 1 auto;
}
/******************************************************** BROWSER ELEMENTS */
body.desktop {
::-webkit-scrollbar {
@ -378,3 +383,25 @@ body.desktop .has-local-controls {
//.--hide-by-default { display: none !important; }
@include responsiveContainerWidths('220');
@include responsiveContainerWidths('600');
.u-fade-truncate,
.u-fade-truncate--lg {
&:after {
display: block;
position: absolute;
top: 0;
bottom: 0;
content: "";
right: 0;
width: $fadeTruncateW * 1.5;
z-index: 2;
}
&.--no-sep {
border-right: none;
}
}
.u-fade-truncate--lg {
flex-basis: 100% !important;
}

View File

@ -453,6 +453,10 @@
white-space: nowrap;
}
@mixin fadeTruncate($color: $colorBodyBg, $angle: 90deg) {
background-image: linear-gradient($angle, transparent 0%, $color 100%);
}
@mixin reverseEllipsis() {
@include ellipsize();
direction: ltr;
@ -585,30 +589,48 @@
}
}
@mixin cButton() {
@include cControl();
@include cControlHov();
@include themedButton();
border-radius: $controlCr;
color: $colorBtnFg;
cursor: pointer;
padding: $interiorMargin floor($interiorMargin * 1.25);
@function cButtonPadding($padding: $interiorMargin, $compact: false) {
@if $compact {
@return floor(math.div($padding, 1.5)) $padding;
} @else {
@return $padding floor($padding * 1.25);
}
}
@mixin cButtonLayout() {
$pad: $interiorMargin;
padding: cButtonPadding($pad);
&:after,
> * + * {
margin-left: $interiorMarginSm;
}
&[class*='--major'],
&[class*='is-active'] {
background: $colorBtnMajorBg;
color: $colorBtnMajorFg;
&[class*='--compact'] {
//padding: floor(math.div($pad, 1.5)) $pad;
padding: cButtonPadding($pad, true);
}
}
&[class*='--caution'] {
background: $colorBtnCautionBg !important;
color: $colorBtnCautionFg !important;
}
@mixin cButton() {
@include cControl();
@include cControlHov();
@include themedButton();
@include cButtonLayout();
border-radius: $controlCr;
color: $colorBtnFg;
cursor: pointer;
&[class*="--major"],
&[class*='is-active']{
background: $colorBtnMajorBg !important;
color: $colorBtnMajorFg !important;
}
&[class*='--caution'] {
background: $colorBtnCautionBg !important;
color: $colorBtnCautionFg !important;
}
}
@mixin cClickIcon() {
@ -640,7 +662,7 @@
*:before {
// *:before handles any nested containers that may contain glyph elements
// Needed for c-togglebutton.
font-size: 1.25em;
font-size: 1.15em;
}
}

View File

@ -570,19 +570,23 @@
display: flex;
flex-direction: column;
overflow: hidden;
padding: $interiorMarginLg;
> * {
flex: 1 1 auto;
&:first-child {
flex: 0 0 auto;
}
}
padding: $interiorMargin $interiorMarginLg;
> * + * {
margin-top: $interiorMargin;
}
.l-browse-bar {
flex: 0 0 auto;
}
.c-snapshots-h__title {
display: flex;
}
.c-snapshots {
flex: 1 1 auto;
}
}
.c-snapshots {

View File

@ -24,7 +24,6 @@
@import '../plugins/telemetryTable/components/table.scss';
@import '../plugins/timeConductor/conductor.scss';
@import '../plugins/timeConductor/conductor-axis.scss';
@import '../plugins/timeConductor/conductor-mode.scss';
@import '../plugins/timeConductor/conductor-mode-icon.scss';
@import '../plugins/timeConductor/date-picker.scss';
@import '../plugins/timeline/timeline.scss';

View File

@ -72,9 +72,8 @@ describe('the url tool', function () {
'tc.mode': 'fixed'
};
const constructedURL = objectPathToUrl(openmct, mockObjectPath, customParams);
expect(constructedURL).toContain(
'tc.startBound=1669911059&tc.endBound=1669911082&tc.mode=fixed'
);
expect(constructedURL).toContain('tc.startBound=1669911059&tc.endBound=1669911082');
expect(constructedURL).toContain('tc.mode=fixed');
});
});
});

View File

@ -55,6 +55,9 @@
'has-complex-content': complexContent
}"
>
<div v-if="supportsIndependentTime" class="c-conductor-holder--compact">
<independent-time-conductor :domain-object="domainObject" :object-path="objectPath" />
</div>
<NotebookMenuSwitcher
v-if="notebookEnabled"
:domain-object="domainObject"
@ -96,15 +99,25 @@
<script>
import ObjectView from './ObjectView.vue';
import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue';
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
import tooltipHelpers from '../../api/tooltips/tooltipMixins';
const SIMPLE_CONTENT_TYPES = ['clock', 'timer', 'summary-widget', 'hyperlink', 'conditionWidget'];
const CSS_WIDTH_LESS_STR = '--width-less-than-';
const SupportedViewTypes = [
'plot-stacked',
'plot-overlay',
'bar-graph.view',
'scatter-plot.view',
'time-strip.view',
'example.imagery'
];
export default {
components: {
ObjectView,
NotebookMenuSwitcher
NotebookMenuSwitcher,
IndependentTimeConductor
},
mixins: [tooltipHelpers],
inject: ['openmct'],
@ -145,7 +158,8 @@ export default {
complexContent,
notebookEnabled: this.openmct.types.get('notebook'),
statusBarItems: [],
status: ''
status: '',
supportsIndependentTime: false
};
},
computed: {
@ -168,6 +182,9 @@ export default {
this.soViewResizeObserver = new ResizeObserver(this.resizeSoView);
this.soViewResizeObserver.observe(this.$refs.soView);
}
const viewKey = this.getViewKey();
this.supportsIndependentTime = this.domainObject && SupportedViewTypes.includes(viewKey);
},
beforeDestroy() {
this.removeStatusListener();
@ -233,6 +250,15 @@ export default {
this.widthClass = wClass.trimStart();
},
getViewKey() {
let viewKey = this.$refs.objectView?.viewKey;
if (this.objectViewKey) {
viewKey = this.objectViewKey;
}
return viewKey;
},
async showToolTip() {
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
this.buildToolTip(await this.getObjectPath(), BELOW, 'objectName');

View File

@ -21,17 +21,6 @@
-->
<template>
<div>
<div
v-if="supportsIndependentTime"
class="c-conductor-holder--compact l-shell__main-independent-time-conductor"
>
<independent-time-conductor
:domain-object="domainObject"
:object-path="path"
@stateChanged="updateIndependentTimeState"
@updated="saveTimeOptions"
/>
</div>
<div ref="objectViewWrapper" class="c-object-view" :class="viewClasses"></div>
</div>
</template>
@ -40,19 +29,11 @@
import _ from 'lodash';
import StyleRuleManager from '@/plugins/condition/StyleRuleManager';
import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
const SupportedViewTypes = [
'plot-stacked',
'plot-overlay',
'bar-graph.view',
'scatter-plot.view',
'time-strip.view'
];
export default {
components: {
IndependentTimeConductor
// IndependentTimeConductor
},
mixins: [stalenessMixin],
inject: ['openmct'],
@ -99,11 +80,6 @@ export default {
font() {
return this.objectFontStyle ? this.objectFontStyle.font : this.layoutFont;
},
supportsIndependentTime() {
const viewKey = this.getViewKey();
return this.domainObject && SupportedViewTypes.includes(viewKey);
},
viewClasses() {
let classes;
@ -509,17 +485,6 @@ export default {
if (elemToStyle !== undefined) {
elemToStyle.dataset.font = newFont;
}
},
//Should the domainObject be updated in the Independent Time conductor component itself?
updateIndependentTimeState(useIndependentTime) {
this.openmct.objects.mutate(
this.domainObject,
'configuration.useIndependentTime',
useIndependentTime
);
},
saveTimeOptions(options) {
this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', options);
}
}
};

View File

@ -110,7 +110,7 @@ export default {
}
},
updateNowMarker() {
if (this.openmct.time.clock() === undefined) {
if (this.openmct.time.getClock() === undefined) {
let nowMarker = document.querySelector('.nowMarker');
if (nowMarker) {
nowMarker.classList.add('hidden');
@ -120,7 +120,7 @@ export default {
if (nowMarker) {
nowMarker.classList.remove('hidden');
nowMarker.style.height = this.contentHeight + 'px';
const nowTimeStamp = this.openmct.time.clock().currentValue();
const nowTimeStamp = this.openmct.time.getClock().currentValue();
const now = this.xScale(nowTimeStamp);
nowMarker.style.left = now + this.offset + 'px';
}
@ -154,7 +154,7 @@ export default {
}
if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem();
timeSystem = this.openmct.time.getTimeSystem();
}
if (timeSystem.isUTCBased) {

View File

@ -15,6 +15,8 @@
.c-object-label {
font-size: 1.05em;
min-width: 20%;
&__type-icon {
opacity: $objectLabelTypeIconOpacity;
}
@ -37,7 +39,8 @@
/*************************** FRAME CONTROLS */
&__frame-controls {
display: flex;
flex: 0 0 auto;
flex: 0 1 auto;
overflow: hidden;
&__btns,
&__more {

View File

@ -1,11 +1,10 @@
.c-object-label {
// <a> tag and draggable element that holds type icon and name.
// Used mostly in trees and lists
@include ellipsize();
display: flex;
align-items: center;
flex: 0 1 auto;
overflow: hidden;
white-space: nowrap;
> * + * {
margin-left: $interiorMargin;

View File

@ -1,9 +1,28 @@
@use 'sass:math';
.c-toggle-switch {
$d: 12px;
$m: 2px;
@mixin toggleSwitch($d: 12px, $m: 2px, $bg: $colorBtnBg) {
$br: math.div($d, 1.5);
.c-toggle-switch__slider {
background: $bg;
border-radius: $br;
height: $d + ($m * 2);
width: $d * 2 + $m * 2;
&:before {
// Knob
border-radius: floor($br * 0.8);
box-shadow: rgba(black, 0.4) 0 0 2px;
height: $d;
width: $d;
top: $m;
left: $m;
right: auto;
}
}
}
.c-toggle-switch {
cursor: pointer;
display: inline-flex;
align-items: center;
@ -20,6 +39,26 @@
display: block;
}
&__slider {
// Sits within __switch
display: inline-block;
position: relative;
&:before {
// Knob
background: $colorBtnFg; // TODO: make discrete theme constants for these colors
content: '';
display: block;
position: absolute;
transition: transform 100ms ease-in-out;
}
}
&__label {
margin-left: $interiorMarginSm;
white-space: nowrap;
}
input {
opacity: 0;
width: 0;
@ -35,34 +74,9 @@
}
}
&__slider {
// Sits within __switch
background: $colorBtnBg; // TODO: make discrete theme constants for these colors
border-radius: $br;
display: inline-block;
height: $d + ($m * 2);
position: relative;
width: $d * 2 + $m * 2;
&:before {
// Knob
background: $colorBtnFg; // TODO: make discrete theme constants for these colors
border-radius: floor($br * 0.8);
box-shadow: rgba(black, 0.4) 0 0 2px;
content: '';
display: block;
position: absolute;
height: $d;
width: $d;
top: $m;
left: $m;
right: auto;
transition: transform 100ms ease-in-out;
}
}
&__label {
margin-left: $interiorMarginSm;
white-space: nowrap;
}
@include toggleSwitch();
}
.c-toggle-switch--mini {
@include toggleSwitch($d: 9px, $m: 0px);
}

View File

@ -49,6 +49,15 @@
</div>
<div class="l-browse-bar__end">
<div
v-if="supportsIndependentTime"
class="c-conductor-holder--compact l-shell__main-independent-time-conductor"
>
<independent-time-conductor
:domain-object="domainObject"
:object-path="openmct.router.path"
/>
</div>
<ViewSwitcher v-if="!isEditing" :current-view="currentView" :views="views" />
<!-- Action buttons -->
<NotebookMenuSwitcher
@ -130,12 +139,21 @@
<script>
import ViewSwitcher from './ViewSwitcher.vue';
import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue';
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
import tooltipHelpers from '../../api/tooltips/tooltipMixins';
const SupportedViewTypes = [
'plot-stacked',
'plot-overlay',
'bar-graph.view',
'time-strip.view',
'example.imagery'
];
const PLACEHOLDER_OBJECT = {};
export default {
components: {
IndependentTimeConductor,
NotebookMenuSwitcher,
ViewSwitcher
},
@ -226,6 +244,11 @@ export default {
} else {
return 'Unlocked for editing - click to lock.';
}
},
supportsIndependentTime() {
const viewKey = this.getViewKey();
return this.domainObject && SupportedViewTypes.includes(viewKey);
}
},
watch: {
@ -300,6 +323,14 @@ export default {
edit() {
this.openmct.editor.edit();
},
getViewKey() {
let viewKey = this.viewKey;
if (this.objectViewKey) {
viewKey = this.objectViewKey;
}
return viewKey;
},
promptUserandCancelEditing() {
let dialog = this.openmct.overlays.dialog({
iconClass: 'alert',

View File

@ -289,17 +289,6 @@
flex: 1 1 auto !important;
}
&__time-conductor {
border-top: 1px solid $colorInteriorBorder;
display: flex;
flex-direction: column;
padding-top: $interiorMargin;
> * + * {
margin-top: $interiorMargin;
}
}
&__main {
> .l-pane {
padding: nth($shellPanePad, 1) 0;
@ -383,10 +372,10 @@
align-items: center;
justify-content: space-between;
[class*='__'] {
// Removes extraneous horizontal white space
display: inline-flex;
}
//[class*="__"] {
// // Removes extraneous horizontal white space
// display: inline-flex;
//}
> * + * {
margin-left: $interiorMarginSm;

View File

@ -1,91 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
class Ticker {
constructor() {
this.callbacks = [];
this.last = new Date() - 1000;
}
/**
* Calls functions every second, as close to the actual second
* tick as is feasible.
* @constructor
* @memberof utils/clock
*/
tick() {
const timestamp = new Date();
const millis = timestamp % 1000;
// Only update callbacks if a second has actually passed.
if (timestamp >= this.last + 1000) {
this.callbacks.forEach(function (callback) {
callback(timestamp);
});
this.last = timestamp - millis;
}
// Try to update at exactly the next second
this.timeoutHandle = setTimeout(
() => {
this.tick();
},
1000 - millis,
true
);
}
/**
* Listen for clock ticks. The provided callback will
* be invoked with the current timestamp (in milliseconds
* since Jan 1 1970) at regular intervals, as near to the
* second boundary as possible.
*
* @param {Function} callback callback to invoke
* @returns {Function} a function to unregister this listener
*/
listen(callback) {
if (this.callbacks.length === 0) {
this.tick();
}
this.callbacks.push(callback);
// Provide immediate feedback
callback(this.last);
// Provide a deregistration function
return () => {
this.callbacks = this.callbacks.filter(function (cb) {
return cb !== callback;
});
if (this.callbacks.length === 0) {
clearTimeout(this.timeoutHandle);
}
};
}
}
let ticker = new Ticker();
export default ticker;

8
src/utils/debounce.js Normal file
View File

@ -0,0 +1,8 @@
export default function debounce(func, delay) {
let debounceTimer;
return function (...args) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func(...args), delay);
};
}

View File

@ -1,12 +1,12 @@
export default function raf(callback) {
let rendering = false;
return () => {
return (...args) => {
if (!rendering) {
rendering = true;
requestAnimationFrame(() => {
callback();
callback(...args);
rendering = false;
});
}

View File

@ -27,7 +27,7 @@ export default class StalenessUtils {
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.lastStalenessResponseTime = 0;
this.setTimeSystem(this.openmct.time.timeSystem());
this.setTimeSystem(this.openmct.time.getTimeSystem());
this.watchTimeSystem();
}

View File

@ -45,7 +45,7 @@ export function createOpenMct(timeSystemOptions = DEFAULT_TIME_OPTIONS) {
const start = timeSystemOptions.bounds.start;
const end = timeSystemOptions.bounds.end;
openmct.time.timeSystem(timeSystemKey, {
openmct.time.setTimeSystem(timeSystemKey, {
start,
end
});