diff --git a/API.md b/API.md index 8f5dc55260d..01ea65aa087 100644 --- a/API.md +++ b/API.md @@ -2,7 +2,7 @@ **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`. diff --git a/e2e/appActions.js b/e2e/appActions.js index bf637903430..56cc0e5a7f9 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -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); } /** diff --git a/e2e/test-data/VisualTestData_storage.json b/e2e/test-data/VisualTestData_storage.json index 02fe3cd82b5..017415dce40 100644 --- a/e2e/test-data/VisualTestData_storage.json +++ b/e2e/test-data/VisualTestData_storage.json @@ -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": "[]" } ] } ] -} +} \ No newline at end of file diff --git a/e2e/test-data/recycled_local_storage.json b/e2e/test-data/recycled_local_storage.json index ab4608e7d81..5af1d40be81 100644 --- a/e2e/test-data/recycled_local_storage.json +++ b/e2e/test-data/recycled_local_storage.json @@ -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}}]" } ] } ] -} +} \ No newline at end of file diff --git a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js index 5c41f2d418f..37840b6b62c 100644 --- a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js @@ -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 }) => { diff --git a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js index 940055bed75..ce16227ad0e 100644 --- a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js @@ -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(); + }); }); diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index 16792644d61..5ef4a6a06bf 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -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(); +} diff --git a/example/imagery/plugin.js b/example/imagery/plugin.js index 573a4a64255..c090c11ea87 100644 --- a/example/imagery/plugin.js +++ b/example/imagery/plugin.js @@ -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 diff --git a/src/MCT.js b/src/MCT.js index a4c43c6701e..50b5c15029a 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -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/'); }); diff --git a/src/api/menu/components/SuperMenu.vue b/src/api/menu/components/SuperMenu.vue index 91d9970c335..60557cb1f6e 100644 --- a/src/api/menu/components/SuperMenu.vue +++ b/src/api/menu/components/SuperMenu.vue @@ -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)" diff --git a/src/api/telemetry/TelemetryAPI.js b/src/api/telemetry/TelemetryAPI.js index fe15d3e8920..2cb27c6a099 100644 --- a/src/api/telemetry/TelemetryAPI.js +++ b/src/api/telemetry/TelemetryAPI.js @@ -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; diff --git a/src/api/telemetry/TelemetryAPISpec.js b/src/api/telemetry/TelemetryAPISpec.js index a2550e978fa..66c8f603f3d 100644 --- a/src/api/telemetry/TelemetryAPISpec.js +++ b/src/api/telemetry/TelemetryAPISpec.js @@ -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 }); }); }); diff --git a/src/api/telemetry/TelemetryCollection.js b/src/api/telemetry/TelemetryCollection.js index 97a7dc16212..3b1fd370cec 100644 --- a/src/api/telemetry/TelemetryCollection.js +++ b/src/api/telemetry/TelemetryCollection.js @@ -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 + ); } /** diff --git a/src/api/time/IndependentTimeContext.js b/src/api/time/IndependentTimeContext.js index c1185742094..dbcec35e23f 100644 --- a/src/api/time/IndependentTimeContext.js +++ b/src/api/time/IndependentTimeContext.js @@ -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; diff --git a/src/api/time/TimeAPI.js b/src/api/time/TimeAPI.js index 617954db3fe..e0e60eaff34 100644 --- a/src/api/time/TimeAPI.js +++ b/src/api/time/TimeAPI.js @@ -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); diff --git a/src/api/time/TimeAPISpec.js b/src/api/time/TimeAPISpec.js index 97300157e26..5b31e90967f 100644 --- a/src/api/time/TimeAPISpec.js +++ b/src/api/time/TimeAPISpec.js @@ -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', () => { diff --git a/src/api/time/TimeContext.js b/src/api/time/TimeContext.js index cef987e3da3..8ff1657696f 100644 --- a/src/api/time/TimeContext.js +++ b/src/api/time/TimeContext.js @@ -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) { + // 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); + } + } + + /** + * 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; } - const newBounds = { - start: timestamp + this.offsets.start, - end: timestamp + this.offsets.end - }; + 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)); + } - this.boundsVal = newBounds; - this.emit('bounds', this.boundsVal, true); + if (offsetsOrBounds !== undefined) { + if (this.isRealTime()) { + this.setClockOffsets(offsetsOrBounds); + } else { + this.setBounds(offsetsOrBounds); + } + } } /** - * Checks if this time context is in real-time mode or not. + * 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); + } + + 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.`; } - return false; + // 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)); } } diff --git a/src/api/time/constants.js b/src/api/time/constants.js new file mode 100644 index 00000000000..85182a574b7 --- /dev/null +++ b/src/api/time/constants.js @@ -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 +}; diff --git a/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js b/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js index 3355dc021af..0dfab710c34 100644 --- a/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js +++ b/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js @@ -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(); - - if (clock === undefined) { - searchParams.set(SEARCH_MODE, MODE_FIXED); + 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 (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; } } diff --git a/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js index 1dd6c18b879..67ce2077f03 100644 --- a/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js +++ b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js @@ -40,7 +40,6 @@ describe('The URLTimeSettingsSynchronizer', () => { }); afterEach(() => { - openmct.time.stopClock(); openmct.router.removeListener('change:hash', resolveFunction); appHolder = undefined; diff --git a/src/plugins/clock/components/Clock.vue b/src/plugins/clock/components/Clock.vue index bb5456e7603..5e76be63e38 100644 --- a/src/plugins/clock/components/Clock.vue +++ b/src/plugins/clock/components/Clock.vue @@ -41,13 +41,13 @@ diff --git a/src/plugins/timeConductor/ConductorHistory.vue b/src/plugins/timeConductor/ConductorHistory.vue index 3aed6944a9e..64f09f4c081 100644 --- a/src/plugins/timeConductor/ConductorHistory.vue +++ b/src/plugins/timeConductor/ConductorHistory.vue @@ -25,6 +25,7 @@