Compare commits

...

19 Commits

Author SHA1 Message Date
094328cd32 source maps 2024-06-24 12:13:02 -07:00
554f77c42f [a11y] re-enable and fix all failing plan tests (#7753)
* enable planning a11y tests

* Fix all a11y failing tests

* drive-by: missed clean

* test: attempt to fix parallelization

* split the test 3 ways

* lint

---------

Co-authored-by: Hill, John (ARC-TI)[KBR Wyle Services, LLC] <john.c.hill@nasa.gov>
2024-06-21 16:37:36 -07:00
a5770817cc chore: re-enable a11y checks in visual suite (#7747)
* re-enable main test

* Enable tests and fix some failing ones

* revert accidental changes

* test(a11y): on failure, take a screenshot to disk

* test(visual): wait for the snapshot indicator to stop flashing

* Fix all failing tests

* test: disable a11y checks for planning suite

* lint:fix

* fix pathing issues

* build: increase visual-a11y parallelism from 2 to 4

* test: darkmatter theme a11y checks + snapshots-- ACTIVATE!

* test: fix file name and path

* fix(#7317): scott bell prophecy

* jk no credit for u

* chore: disable parallelism until we figure out what's going on

* chore: set parallelism to 2 in hopes it fixes percy

---------

Co-authored-by: Hill, John (ARC-TI)[KBR Wyle Services, LLC] <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-06-19 01:16:54 +00:00
34b4091204 fix(#7680): no index.html on npm pack (#7699) 2024-06-05 19:11:54 +00:00
6360bc4b6c Ensure time conductor mode is set when synchronizing time range (#7731)
* Use setMode API to set the time span as well as the bounds instead of the old bounds time API.

* Add test for synchronized time conductor via plots

* Fix linting issue
2024-06-03 16:17:41 +00:00
c354e1c2f1 If display bounds are out of sync with time conductor, don't purge data out of bounds (#7732)
* If we're paused, don't purge data out of bounds
* Refactor auto scale utility functions for reuse
* Create new test spec for plot controls
* Move plot related actions into it's own file
* Fix typo for imports
* Remove named exposedFunction as it is causing errors when the method is used on the same page more than once.
* Fix spelling
2024-06-03 15:46:05 +00:00
eba6f0f505 fix(#7670): Ensure objects unsubscribe from staleness when destroyed (#7736)
* clear any staleness subscriptions before viewing a new object

* add checkbox to options for createDomainObjectWithDefaults, update swg staleness provider to use new time api, add test

* revert

* modified for simplification and to remove the need to update helper methods

* consistent naming convention

* Update e2e/tests/functional/staleness.e2e.spec.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* adding back vars that were removed

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-05-31 20:46:59 +00:00
017380bb6a chore: upgrade eventemitter to v5.0.2 (#7709)
* chore: upgrade eventemitter to v5.0.2
* fix: pass context to eventHelpers
* fix: no need to destroy router as it is destroyed during openmct teardown
* fix: register `CreateAction` and retrieve it from the registry
* test: fix tests
* refactor: import action key consts
* fix: update usage. don't use getters
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2024-05-14 21:51:33 +00:00
810d580b18 [Telemetry Tables] Make sure tables auto scroll correctly on first load (#7720)
* run scroll method to scroll to top after initial load of historical data

* clarifying comment

* added e2e test to make sure tables auto scroll on mount

* adding descriptive comments

* adding in ascending check as well

* added new appAction for navigating to/in realtime, using it in table scroll test

* lint

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2024-05-09 11:53:11 -07:00
977792fae8 this is 2024. * observers no more. (#7715)
* this is 2024. `*` observers no more.

* add edit and save domain object helper functions

* add aria-labels and fix e2e tests to use new labels

* generate and save in local storage a condition set with telemetry and condition

* rename const

* move creation code out of generateLocalStorage since it is immutable

* remove function abstractions

* remove @localStorage test label

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* remove unneccesary aria text

* remove unneccessary aria text

* use recommended playwright locators

* lint fix

* remove unneccesary steps now that child created directly in parent

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-05-09 11:19:55 -07:00
a69e300f1c chore: bump playwright to v1.44.0 (#7716) 2024-05-09 08:20:31 -07:00
17bc6cb722 chore(webpack): destruct version from pkgJson (#7713) 2024-05-07 08:52:41 -07:00
b3d3465734 [Darkmatter] Create new darkmatter theme (#7682)
* initial theme plugin setup, changes to layout frames

* update visual tests

* Changes to gauge, layout borders, and background

* Make background image a DIY theme variable. Fixes made to gauges. Deleted custom font.

* More changes to overall background colors. Added glass layer effect to menus

* changes to menu

* Fix to make theme easy to run

* Fix tab colors and add glass background to menus

* make highlightd corners longer

* Initial changes to font styles

* Add temporary numeric font style. Test numeric font in gauges.

* Initial changes to alphanumerics in layouts

* Updated variables

* update plugin.js file

* Fix highlighted corners on frames such that it uses outermost frame

* renaming theme plugin and rename branch

* fix button colors to be more readable

* change background image

* Fix bad merges from other theme files. Fix gauge and alphanumerics such that they dont have darkmatter borders

* more fixes

* Fix where mixin is used such that when an object's frame is hidden, highlgihts disappear

* remove blur from meter gauges

* Add comment about this theme being in beta mode

* Delete draft .scss file that is no longer needed

* Fix major accessibility issues

* Fix PR review comments

*  fix: Correct import file name for DarkMatter theme.

* Fix other theme code that was failing e2e tests

* Revert index.html

* Fix linting error

* Fix for failing percy test regarding padding

* Fix for failing percy test regarding padding part 2

* Fix for failing percy test regarding padding part 3

* Remove mixin that may be causing percy issue

* Another fix to resolve percy issue

* Add back some code that was deleted during debugging, and create new variables for the object padding

* Fix gradient clipping in inspector

* Restructure all constants-.scss files

* Change bg image to be square and NASA official picture

* Final fixes to darkmatter variable layouts

* Address PR comments

* Change darkmatter to darkmatterTheme

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-04-25 16:06:07 -07:00
fb0d74e87f chore(deps-dev): bump vue from 3.4.19 to 3.4.24 (#7702)
Bumps [vue](https://github.com/vuejs/core) from 3.4.19 to 3.4.24.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.4.19...v3.4.24)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 13:19:00 -07:00
a961d7e3bf Fix nested Flexible Layout direction problem (#7637)
* Closes #7635
- More specific approach to CSS class application for column vs. row layouts.
- Added layout direction CSS classing to `c-fl-container__frames-holder`.
- Switched toolbar icon and titling for better parity with
'toggle' approach used elsewhere.
- Cleaned up duped property def in mixin.

* Addressing PR change requests
- Updated e2e test.
- New computed properties for layout direction.
- CSS code cleanup.

* fix selector in test

* fix more bad selectors

* fix changed title

---------

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
2024-04-18 23:38:11 +00:00
5a06b51c5a refactor: remove the final .bounds() call (#7695)
 refactor: Use getBounds method instead of bounds in IndependentTimeContext.
2024-04-17 16:19:21 +00:00
ef8b353d01 Improve performance of JSON import and add progress bar (#7654)
* add progress bar

* improves performance a lot

* fix test

* remove old function

* fix legit bug from test - well done tests

* add constant, better comments

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-04-16 21:23:31 +00:00
6c5b925454 chore: remove all usage of deprecated Time API methods (#7688)
* chore: remove all usage of deprecated Time API methods

* test: update unit test

* docs: Fix spacing and add clarity to TimeConductorBounds definition.

* test: add unit test coverage for new time api methods
2024-04-16 21:12:09 +00:00
e91aba2e37 Handle paste events for images and text properly (#7679)
* enable eval source maps for debugging

* split image and text paste handling
better event handling

* change back source maps

* image takes precedence over text

* break up notebook entry functions for re-use

* create hotkeys utils
add clipboard functions

* add notebook paste test

* add test for pasting to selected but not editing entry

* link tests to issue

* jsdoc addition

* jsdocs

* no need to import then export

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* fix changed path

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-04-16 13:54:40 -07:00
163 changed files with 4306 additions and 2218 deletions

View File

@ -5,11 +5,11 @@ orbs:
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.42.1-focal
- image: mcr.microsoft.com/playwright:v1.44.0-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_POSTINSTALL_BROWSER: "true" # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_PARALLEL_TOTAL: 2
ubuntu:
machine:
@ -17,7 +17,7 @@ executors:
docker_layer_caching: true
commands:
build_and_install:
description: 'All steps used to build and install.'
description: "All steps used to build and install."
parameters:
node-version:
type: string
@ -27,7 +27,7 @@ commands:
node-version: << parameters.node-version >>
- node/install-packages
generate_and_store_version_and_filesystem_artifacts:
description: 'Track important packages and files'
description: "Track important packages and files"
steps:
- run: |
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
@ -38,7 +38,7 @@ commands:
- store_artifacts:
path: /tmp/artifacts/
generate_e2e_code_cov_report:
description: 'Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test'
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
parameters:
suite:
type: string
@ -102,7 +102,7 @@ jobs:
node-version: lts/hydrogen
- when: #Only install chrome-beta when running the 'full' suite to save $$$
condition:
equal: ['full', <<parameters.suite>>]
equal: ["full", <<parameters.suite>>]
steps:
- run: npx playwright install chrome-beta
- run:
@ -159,7 +159,7 @@ jobs:
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npx playwright@1.42.1 install #Necessary for bare ubuntu machine
- run: npx playwright@1.44.0 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@ -230,7 +230,7 @@ jobs:
steps:
- build_and_install:
node-version: lts/iron
- run: npm run test:e2e:visual:<<parameters.suite>>
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:visual:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
- store_test_results:
path: test-results/results.xml
- store_artifacts:
@ -282,7 +282,7 @@ workflows:
- e2e-couchdb
triggers:
- schedule:
cron: '0 0 * * *'
cron: "0 0 * * *"
filters:
branches:
only:

View File

@ -497,7 +497,8 @@
"checksnapshots",
"specced",
"composables",
"countup"
"countup",
"darkmatter"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"ignorePaths": [

View File

@ -37,7 +37,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.42.1 install
- run: npx playwright@1.44.0 install
- name: Start CouchDB Docker Container and Init with Setup Scripts
run: |

View File

@ -30,7 +30,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.42.1 install
- run: npx playwright@1.44.0 install
- run: npm ci --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times)

View File

@ -28,7 +28,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.42.1 install
- run: npx playwright@1.44.0 install
- run: npm ci --no-audit --progress=false
- run: npm run test:perf:localhost
- run: npm run test:perf:contract

View File

@ -33,7 +33,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.42.1 install
- run: npx playwright@1.44.0 install
- run: npx playwright install chrome-beta
- run: npm ci --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40

View File

@ -22,3 +22,6 @@
!index.html
!openmct.js
!SECURITY.md
# Dont include the example html
dist/index.html

View File

@ -19,7 +19,7 @@ import { merge } from 'webpack-merge';
let gitRevision = 'error-retrieving-revision';
let gitBranch = 'error-retrieving-branch';
const packageDefinition = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
const { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
try {
gitRevision = execSync('git rev-parse HEAD').toString().trim();
@ -49,7 +49,8 @@ const config = {
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
espressoTheme: './src/plugins/themes/espresso-theme.scss',
snowTheme: './src/plugins/themes/snow-theme.scss'
snowTheme: './src/plugins/themes/snow-theme.scss',
darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss'
},
output: {
globalObject: 'this',
@ -84,7 +85,7 @@ const config = {
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
__OPENMCT_VERSION__: `'${version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,

View File

@ -6,7 +6,7 @@ information to pull requests.
import config from './webpack.dev.mjs';
config.devtool = 'source-map';
config.devtool = 'inline-source-map';
config.devServer.hot = false;
config.module.rules.push({

View File

@ -15,5 +15,5 @@ export default merge(common, {
__OPENMCT_ROOT_RELATIVE__: '""'
})
],
devtool: 'source-map'
devtool: 'eval-source-map'
});

View File

@ -275,6 +275,17 @@ async function navigateToObjectWithFixedTimeBounds(page, url, start, end) {
);
}
/**
* Navigates directly to a given object url, in real-time mode.
* @param {import('@playwright/test').Page} page
* @param {string} url The url to the domainObject
*/
async function navigateToObjectWithRealTime(page, url, start = '1800000', end = '30000') {
await page.goto(
`${url}?tc.mode=local&tc.startDelta=${start}&tc.endDelta=${end}&tc.timeSystem=utc`
);
}
/**
* Open the given `domainObject`'s context menu from the object tree.
* Expands the path to the object and scrolls to it if necessary.
@ -581,9 +592,6 @@ async function waitForPlotsToRender(page) {
* @return {Promise<PlotPixel[]>}
*/
async function getCanvasPixels(page, canvasSelector) {
const getTelemValuePromise = new Promise((resolve) =>
page.exposeFunction('getCanvasValue', resolve)
);
const canvasHandle = await page.evaluateHandle(
(canvas) => document.querySelector(canvas),
canvasSelector
@ -594,7 +602,7 @@ async function getCanvasPixels(page, canvasSelector) {
);
await waitForPlotsToRender(page);
await page.evaluate(
return page.evaluate(
([canvas, ctx]) => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
@ -622,12 +630,10 @@ async function getCanvasPixels(page, canvasSelector) {
i = i + 4;
}
window.getCanvasValue(plotPixels);
return plotPixels;
},
[canvasHandle, canvasContextHandle]
);
return getTelemValuePromise;
}
/**
@ -656,6 +662,7 @@ export {
getFocusedObjectUuid,
getHashUrlToDomainObject,
navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime,
openObjectTreeContextMenu,
renameObjectFromContextMenu,
setEndOffset,

View File

@ -34,7 +34,7 @@
*/
import AxeBuilder from '@axe-core/playwright';
import fs from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
@ -87,6 +87,27 @@ const extendedTest = test.extend({
}
});
/**
* Writes the accessibility report to the specified path.
*
* @param {string} reportPath - The path to write the report to.
* @param {Object} accessibilityScanResults - The results of the accessibility scan.
* @returns {Promise<Object>} The accessibility scan results.
* @throws Will throw an error if writing the report fails.
*/
async function writeAccessibilityReport(reportPath, accessibilityScanResults) {
try {
await fs.mkdir(path.dirname(reportPath), { recursive: true });
const data = JSON.stringify(accessibilityScanResults, null, 2);
await fs.writeFile(reportPath, data);
console.log(`Accessibility report with violations saved successfully as ${reportPath}`);
return accessibilityScanResults;
} catch (err) {
console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
throw err;
}
}
/**
* Scans for accessibility violations on a page and writes a report to disk if violations are found.
* Automatically asserts that no violations should be present.
@ -104,25 +125,29 @@ export async function scanForA11yViolations(page, testCaseName, options = {}) {
const accessibilityScanResults = await builder.analyze();
// Assert that no violations should be present
expect(
accessibilityScanResults.violations,
`Accessibility violations found in test case: ${testCaseName}`
).toEqual([]);
expect
.soft(
accessibilityScanResults.violations,
`Accessibility violations found in test case: ${testCaseName}`
)
.toEqual([]);
// Check if there are any violations
if (accessibilityScanResults.violations.length > 0) {
let reportName = options.reportName || testCaseName;
let sanitizedReportName = reportName.replace(/\//g, '_');
const reportPath = path.join(TEST_RESULTS_DIR, `${sanitizedReportName}.json`);
const reportName = options.reportName || testCaseName;
const sanitizedReportName = reportName.replace(/\//g, '_');
const reportPath = path.join(
TEST_RESULTS_DIR,
'a11y-json-reports',
`${sanitizedReportName}.json`
);
try {
if (!fs.existsSync(TEST_RESULTS_DIR)) {
fs.mkdirSync(TEST_RESULTS_DIR);
}
await page.screenshot({
path: path.join(TEST_RESULTS_DIR, 'a11y-screenshots', `${sanitizedReportName}.png`)
});
fs.writeFileSync(reportPath, JSON.stringify(accessibilityScanResults, null, 2));
console.log(`Accessibility report with violations saved successfully as ${reportPath}`);
return accessibilityScanResults;
return await writeAccessibilityReport(reportPath, accessibilityScanResults);
} catch (err) {
console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
throw err;

View File

@ -0,0 +1,47 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const isMac = process.platform === 'darwin';
const modifier = isMac ? 'Meta' : 'Control';
/**
* @param {import('@playwright/test').Page} page
*/
async function selectAll(page) {
await page.keyboard.press(`${modifier}+KeyA`);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function copy(page) {
await page.keyboard.press(`${modifier}+KeyC`);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function paste(page) {
await page.keyboard.press(`${modifier}+KeyV`);
}
export { copy, paste, selectAll };

View File

@ -0,0 +1,23 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
export * from './clipboard.js';

View File

@ -28,16 +28,28 @@ import { fileURLToPath } from 'url';
/**
* @param {import('@playwright/test').Page} page
* @param {string} text
*/
async function enterTextEntry(page, text) {
// Click the 'Add Notebook Entry' area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.getByLabel('Notebook Entry Input').last().fill(text);
await addNotebookEntry(page);
await enterTextInLastEntry(page, text);
await commitEntry(page);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function addNotebookEntry(page) {
await page.locator(NOTEBOOK_DROP_AREA).click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextInLastEntry(page, text) {
await page.getByLabel('Notebook Entry Input').last().fill(text);
}
/**
* @param {import('@playwright/test').Page} page
*/
@ -140,10 +152,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
}
export {
addNotebookEntry,
commitEntry,
createNotebookAndEntry,
createNotebookEntryAndTags,
dragAndDropEmbed,
enterTextEntry,
enterTextInLastEntry,
lockPage,
startAndAddRestrictedNotebookObject
};

View File

@ -0,0 +1,29 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
// This should be used to install the Darkmatter theme for Open MCT.
// e.g.
// await page.addInitScript({ path: path.join(__dirname, 'useDarkmatterTheme.js') });
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.DarkmatterTheme());
});

View File

@ -18,7 +18,7 @@
"@types/sinonjs__fake-timers": "8.1.5",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.42.1",
"@playwright/test": "1.44.0",
"@axe-core/playwright": "4.8.5",
"sinon": "17.0.0"
},

View File

@ -36,6 +36,13 @@ const config = {
browserName: 'chromium',
theme: 'snow'
}
},
{
name: 'darkmatter-theme', //Runs the same visual tests but with darkmatter-theme
use: {
browserName: 'chromium',
theme: 'darkmatter'
}
}
],
reporter: [

View File

@ -127,6 +127,11 @@ const extendedTest = test.extend({
await page.addInitScript({
path: fileURLToPath(new URL('./helper/useSnowTheme.js', import.meta.url))
});
} else if (theme === 'darkmatter') {
//inject darkmatter theme
await page.addInitScript({
path: fileURLToPath(new URL('./helper/useDarkmatterTheme.js', import.meta.url))
});
}
// Attach info about the currently running test and its project.

View File

@ -371,7 +371,7 @@ test.describe('Basic Condition Set Use', () => {
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.locator('[aria-label="Current Output Value"]');
let outputValue = page.getByLabel('Current Output Value');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
@ -462,7 +462,7 @@ test.describe('Basic Condition Set Use', () => {
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.locator('[aria-label="Current Output Value"]');
let outputValue = page.getByLabel('Current Output Value');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
@ -475,3 +475,81 @@ test.describe('Basic Condition Set Use', () => {
});
});
});
test.describe('Condition Set Composition', () => {
let conditionSet;
let exampleTelemetry;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Condition Set
conditionSet = await createDomainObjectWithDefaults(page, {
type: 'Condition Set'
});
// Create Telemetry Object as child to Condition Set
exampleTelemetry = await createExampleTelemetryObject(page, conditionSet.uuid);
// Edit Condition Set
await page.goto(conditionSet.url);
await page.getByRole('button', { name: 'Edit Object' }).click();
// Add Condition to Condition Set
await page.getByRole('button', { name: 'Add Condition' }).click();
// Enter Condition Output
await page.getByLabel('Condition Name Input').first().fill('Negative');
await page.getByLabel('Condition Output Type').first().selectOption({ value: 'string' });
await page.getByLabel('Condition Output String').first().fill('Negative');
// Condition Trigger default is okay so no change needed to form
// Enter Condition Criterion
await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ value: 'all' });
await page.getByLabel('Criterion Metadata Selection').first().selectOption({ value: 'sin' });
await page
.locator('select[aria-label="Criterion Comparison Selection"]')
.first()
.selectOption({ value: 'lessThan' });
await page.getByLabel('Criterion Input').first().fill('0');
// Save the Condition Set
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
});
test('You can remove telemetry from a condition set with existing conditions', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7710'
});
await page.getByLabel('Expand My Items folder').click();
await page.getByLabel(`Expand ${conditionSet.name} conditionSet`).click();
await page
.getByLabel(`Navigate to ${exampleTelemetry.name}`, { exact: false })
.click({ button: 'right' });
await page
.getByLabel(`${exampleTelemetry.name} Context Menu`)
.getByRole('menuitem', { name: 'Remove' })
.click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await page
.getByLabel(`Navigate to ${conditionSet.name} conditionSet Object`, { exact: true })
.click();
await page.getByRole('button', { name: 'Edit Object' }).click();
await page.getByRole('tab', { name: 'Elements' }).click();
expect(
await page
.getByRole('tabpanel', { name: 'Inspector Views' })
.getByRole('listitem', { name: exampleTelemetry.name })
.count()
).toEqual(0);
});
});

View File

@ -78,8 +78,8 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));
// Check that panes can be dragged while Flexible Layout is in Edit mode
let dragWrapper = page
.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper')
@ -105,8 +105,8 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));
// Click on the first frame to select it
await page.locator('.c-fl-container__frame').first().click();
@ -122,7 +122,7 @@ test.describe('Flexible Layout', () => {
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
// Change the layout to rows orientation
await page.getByTitle('Columns layout').click();
await page.getByTitle('Switch to rows layout').click();
// Assert the layout is in rows orientation
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
@ -171,7 +171,7 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -202,7 +202,7 @@ test.describe('Flexible Layout', () => {
// 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
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -242,7 +242,7 @@ test.describe('Flexible Layout', () => {
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 exampleImageryTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -309,9 +309,9 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
await page.getByRole('columnheader', { name: 'Container Handle 1' }).click();
const flexRows = page.getByLabel('Flexible Layout Row');
expect(await flexRows.count()).toEqual(0);
await page.getByTitle('Columns layout').click();
await page.getByTitle('Switch to rows layout').click();
expect(await flexRows.count()).toEqual(1);
await page.getByTitle('Rows layout').click();
await page.getByTitle('Switch to columns layout').click();
expect(await flexRows.count()).toEqual(0);
});
});

View File

@ -27,6 +27,7 @@ This test suite is dedicated to tests which verify the basic operations surround
import { fileURLToPath } from 'url';
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
import * as nbUtils from '../../../../helper/notebookUtils.js';
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
@ -546,4 +547,53 @@ test.describe('Notebook entry tests', () => {
);
await expect(secondLineOfBlockquoteText).toBeVisible();
});
/**
* Paste into notebook entry tests
*/
test('Can paste text into a notebook entry', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7686'
});
const TEST_TEXT = 'This is a test';
const iterations = 20;
const EXPECTED_TEXT = TEST_TEXT.repeat(iterations);
await page.goto(notebookObject.url);
await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
await selectAll(page);
await copy(page);
for (let i = 0; i < iterations; i++) {
await paste(page);
}
await nbUtils.commitEntry(page);
await expect(page.locator(`text="${EXPECTED_TEXT}"`)).toBeVisible();
});
test('Prevents pasting text into selected notebook entry if not editing', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7686'
});
const TEST_TEXT = 'This is a test';
await page.goto(notebookObject.url);
await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
await selectAll(page);
await copy(page);
await paste(page);
await nbUtils.commitEntry(page);
// This should not paste text into the entry
await paste(page);
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
});
});

View File

@ -26,6 +26,7 @@ Testsuite for plot autoscale.
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
import { setUserDefinedMinAndMax, turnOffAutoscale } from './plotActions.js';
test.use({
viewport: {
width: 1280,
@ -127,26 +128,6 @@ test.describe('Autoscale', () => {
});
});
/**
* @param {import('@playwright/test').Page} page
*/
async function turnOffAutoscale(page) {
// uncheck autoscale
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} min
* @param {string} max
*/
async function setUserDefinedMinAndMax(page, min, max) {
// set minimum value
await page.getByRole('spinbutton').first().fill(min);
// set maximum value
await page.getByRole('spinbutton').nth(1).fill(max);
}
/**
* @param {import('@playwright/test').Page} page
*/

View File

@ -0,0 +1,42 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
/**
* @param {import('@playwright/test').Page} page
*/
async function turnOffAutoscale(page) {
// uncheck autoscale
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} min
* @param {string} max
*/
async function setUserDefinedMinAndMax(page, min, max) {
// set minimum value
await page.getByRole('spinbutton').first().fill(min);
// set maximum value
await page.getByRole('spinbutton').nth(1).fill(max);
}
export { setUserDefinedMinAndMax, turnOffAutoscale };

View File

@ -0,0 +1,116 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
/*
* This test suite is dedicated to testing the rendering and interaction of plots.
*
*/
import {
createDomainObjectWithDefaults,
getCanvasPixels,
setEndOffset,
setRealTimeMode,
setStartOffset
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
import { setUserDefinedMinAndMax, turnOffAutoscale } from './plotActions.js';
test.describe('Plot Controls', () => {
let overlayPlot;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
// Create an overlay plot with a sine wave generator
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(`${overlayPlot.url}`);
});
test("Plots don't purge data when paused", async ({ page }) => {
// Set realtime mode with 2 second window
const startOffset = {
startMins: '00',
startSecs: '01'
};
const endOffset = {
endMins: '00',
endSecs: '01'
};
// Switch to real-time mode
await setRealTimeMode(page);
// Set start time offset
await setStartOffset(page, startOffset);
// Set end time offset
await setEndOffset(page, endOffset);
// Edit the overlay plot and turn off auto scale, setting the min and max to -1 and 1
// enter edit mode
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
await turnOffAutoscale(page);
await setUserDefinedMinAndMax(page, '-1', '1');
// save
await page.click('button[title="Save"]');
await Promise.all([
page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// click on pause control
await page.getByTitle('Pause incoming real-time data').click();
// expect plot to be paused
await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();
// Wait for 2 seconds to stabilize plot data - future timestamp
// eslint-disable-next-line
await page.waitForTimeout(2000);
// Capture the # of plot points
const plotPixels = await getCanvasPixels(page, 'canvas');
const plotPixelSizeAtPause = plotPixels.length;
// Wait 2 seconds
// eslint-disable-next-line
await page.waitForTimeout(2000);
// Capture the # of plot points
const plotPixelsAfterWait = await getCanvasPixels(page, 'canvas');
const plotPixelSizeAfterWait = plotPixelsAfterWait.length;
// Expect before and after plot points to match
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
});
});

View File

@ -25,7 +25,11 @@
*
*/
import { createDomainObjectWithDefaults, getCanvasPixels } from '../../../../appActions.js';
import {
createDomainObjectWithDefaults,
getCanvasPixels,
setRealTimeMode
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Plot Rendering', () => {
@ -50,6 +54,34 @@ test.describe('Plot Rendering', () => {
createMineFolderRequests.push(req);
});
expect(createMineFolderRequests.length).toEqual(0);
await page.getByLabel('Plot Canvas').hover();
});
test('Time conductor synchronizes with plot time range when that plot control is clicked', async ({
page
}) => {
// Navigate to Sine Wave Generator
await page.goto(sineWaveGeneratorObject.url);
// Switch to real-time mode
await setRealTimeMode(page);
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// click on pause control
await page.getByTitle('Pause incoming real-time data').click();
// expect plot to be paused
await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// click on synchronize with time conductor
await page.getByTitle('Synchronize Time Conductor').click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
//confirm that you're now in fixed mode with the correct range
await expect(page.getByLabel('Time Conductor Mode')).toHaveText('Fixed Timespan');
});
test.fixme('Plot is rendered when infinity values exist', async ({ page }) => {

View File

@ -22,8 +22,8 @@
import {
createDomainObjectWithDefaults,
setTimeConductorBounds,
setTimeConductorMode
navigateToObjectWithRealTime,
setTimeConductorBounds
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
@ -39,12 +39,52 @@ test.describe('Telemetry Table', () => {
type: 'Sine Wave Generator',
parent: table.uuid
});
await page.goto(table.url);
await setTimeConductorMode(page, false);
await navigateToObjectWithRealTime(page, table.url);
const rows = page.getByLabel('table content').getByLabel('Table Row');
await expect(rows).toHaveCount(50);
});
test('on load, auto scrolls to top for descending, and to bottom for ascending', async ({
page
}) => {
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: table.uuid
});
// verify in telemetry table object view
await navigateToObjectWithRealTime(page, table.url);
expect(await getScrollPosition(page)).toBe(0);
// verify in telemetry table view
await page.goto(sineWaveGenerator.url);
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByText('Telemetry Table', { exact: true }).click();
expect(await getScrollPosition(page)).toBe(0);
// navigate back to table
await page.goto(table.url);
// go into edit mode
await page.getByLabel('Edit Object').click();
// change sort direction
await page.locator('thead div').filter({ hasText: 'Time' }).click();
// save view
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// navigate away and back
await page.goto(sineWaveGenerator.url);
await page.goto(table.url);
// verify scroll position
expect(await getScrollPosition(page, false)).toBeLessThan(1);
});
test('unpauses and filters data when paused by button and user changes bounds', async ({
page
}) => {
@ -183,3 +223,42 @@ test.describe('Telemetry Table', () => {
await page.click('button[title="Pause"]');
});
});
async function getScrollPosition(page, top = true) {
const tableBody = page.locator('.c-table__body-w');
// Wait for the scrollbar to appear
await tableBody.evaluate((node) => {
return new Promise((resolve) => {
function checkScroll() {
if (node.scrollHeight > node.clientHeight) {
resolve();
} else {
setTimeout(checkScroll, 100);
}
}
checkScroll();
});
});
// make sure there are rows
const rows = page.getByLabel('table content').getByLabel('Table Row');
await rows.first().waitFor();
// Using this to allow for rows to come and go, so we can truly test the scroll position
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
const { scrollTop, clientHeight, scrollHeight } = await tableBody.evaluate((node) => ({
scrollTop: node.scrollTop,
clientHeight: node.clientHeight,
scrollHeight: node.scrollHeight
}));
// eslint-disable-next-line playwright/no-conditional-in-test
if (top) {
return scrollTop;
} else {
return Math.abs(scrollHeight - (scrollTop + clientHeight));
}
}

View File

@ -0,0 +1,64 @@
/*****************************************************************************
* 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 { createDomainObjectWithDefaults, navigateToObjectWithRealTime } from '../../appActions.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('Staleness', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Does not show staleness after navigating from a stale object', async ({ page }) => {
const objectViewSelector = '.c-object-view';
const isStaleClass = 'is-stale';
const staleSWG = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'SWG'
});
// edit properties and enable staleness updates
await page.getByLabel('More actions').click();
await page.getByLabel('Edit properties...').click();
await page.getByLabel('Provide Staleness Updates', { exact: true }).click();
await page.getByLabel('Save').click();
const folder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder 1'
});
// Navigate to the stale object
await navigateToObjectWithRealTime(page, staleSWG.url);
// Assert that staleness is shown
await expect(page.locator(`${objectViewSelector} .${isStaleClass}`)).toBeAttached({
timeout: 30 * 1000 // Give 30 seconds for the staleness to be updated
});
// Immediately navigate to the folder
await page.goto(folder.url);
// Verify that staleness is not shown
await expect(page.locator(`${objectViewSelector} .${isStaleClass}`)).not.toBeAttached();
});
});

View File

@ -265,8 +265,8 @@ test.describe('Verify tooltips', () => {
name: 'Test Flexible Layout'
});
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl__container >> nth=0');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl__container >> nth=1');
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl-container >> nth=0');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl-container >> nth=1');
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();

View File

@ -20,15 +20,14 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { test } from '../../avpFixtures.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js';
test.describe('a11y - Default', () => {
test.describe('a11y - Default @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
test('main view', async ({ page }, testInfo) => {
//Skipping for https://github.com/nasa/openmct/issues/7421
//await scanForA11yViolations(page, testInfo.title);
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@ -27,7 +27,7 @@ Tests the branding associated with the default deployment. At least the about mo
import percySnapshot from '@percy/playwright';
import { fileURLToPath } from 'url';
import { expect, test } from '../../../avpFixtures.js';
import { expect, scanForA11yViolations, test } from '../../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../../constants.js';
//Declare the component scope of the visual test for Percy
@ -69,14 +69,26 @@ test.describe('Visual - Header @a11y', () => {
});
test('show snapshot button', async ({ page, theme }) => {
test.slow(true, 'We have to wait for the snapshot indicator to stop flashing');
await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await expect(page.getByLabel('Show Snapshots')).toBeVisible();
/**
* We have to wait for the snapshot indicator to stop flashing. This happens
* for a really long time (15 seconds 😳).
* TODO: Either reduce the length of the animation, convert this to a
* Playwright snapshot test (and disable animations), or augment the `waitForAnimations`
* fixture to adjust the timeout.
*/
await expect(page.locator('.has-new-snapshot')).not.toBeAttached({
timeout: 30 * 1000
});
await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {
scope: header
});
await expect(page.getByLabel('Show Snapshots')).toBeVisible();
});
});
@ -99,7 +111,6 @@ test.describe('Mission Header @a11y', () => {
});
});
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -22,7 +22,7 @@
import percySnapshot from '@percy/playwright';
import { test } from '../../../avpFixtures.js';
import { scanForA11yViolations, test } from '../../../avpFixtures.js';
import { MISSION_TIME, VISUAL_FIXED_URL } from '../../../constants.js';
//Declare the scope of the visual test
@ -55,7 +55,6 @@ test.describe('Visual - Inspector @ally @clock', () => {
});
});
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -23,7 +23,7 @@
import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults, expandTreePaneItemByName } from '../../appActions.js';
import { expect, test } from '../../avpFixtures.js';
import { expect, scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js';
import { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js';
@ -163,8 +163,7 @@ test.describe('Visual - Notebook @a11y', () => {
// Take a snapshot
await percySnapshot(page, `Notebook Selected Entry Text Area Active (theme: '${theme}')`);
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});
});

View File

@ -24,91 +24,15 @@ import percySnapshot from '@percy/playwright';
import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
import { test } from '../../avpFixtures.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getFirstActivity,
setBoundsToSpanAllActivities,
setDraftStatusForPlan
} from '../../helper/planningUtils.js';
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url))
);
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
const examplePlanSmall2 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
);
test.describe('Visual - Timelist progress bar @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
await page.getByLabel('Click to collapse items').click();
});
test('progress pie is full', async ({ page, theme }) => {
// Progress pie is completely full and doesn't update if now is greater than the end time
await percySnapshot(page, `Time List with Activity in Progress (theme: ${theme})`);
});
});
test.describe('Visual - Planning', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
test('Plan View', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`);
});
test('Resize Plan View @2p', async ({ browser, theme }) => {
// need to set viewport to null to allow for resizing
const newContext = await browser.newContext({
viewport: null
});
const newPage = await newContext.newPage();
await newPage.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
const plan = await createPlanFromJSON(newPage, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(newPage, examplePlanSmall2, plan.url);
// resize the window
await newPage.setViewportSize({ width: 800, height: 600 });
await percySnapshot(newPage, `Plan View resized (theme: ${theme})`);
});
test('Plan View w/ draft status', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test (Draft)',
json: examplePlanSmall2
});
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
await setDraftStatusForPlan(page, plan);
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`);
});
});
test.describe('Visual - Gantt Chart', () => {
test.describe('Visual - Gantt Chart @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
@ -179,7 +103,6 @@ test.describe('Visual - Gantt Chart', () => {
});
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -0,0 +1,59 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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 percySnapshot from '@percy/playwright';
import fs from 'fs';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getFirstActivity
} from '../../helper/planningUtils.js';
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url))
);
test.describe('Visual - Timelist progress bar @clock @a11y', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
await page.getByLabel('Click to collapse items').click();
});
test('progress pie is full', async ({ page, theme }) => {
// Progress pie is completely full and doesn't update if now is greater than the end time
await percySnapshot(page, `Time List with Activity in Progress (theme: ${theme})`);
});
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -0,0 +1,113 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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 percySnapshot from '@percy/playwright';
import fs from 'fs';
import { createPlanFromJSON } from '../../appActions.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getFirstActivity,
setBoundsToSpanAllActivities,
setDraftStatusForPlan
} from '../../helper/planningUtils.js';
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url))
);
const examplePlanSmall2 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
);
test.describe('Visual - Timelist progress bar @clock @a11y', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
await page.getByLabel('Click to collapse items').click();
});
test('progress pie is full', async ({ page, theme }) => {
// Progress pie is completely full and doesn't update if now is greater than the end time
await percySnapshot(page, `Time List with Activity in Progress (theme: ${theme})`);
});
});
test.describe('Visual - Plan View @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
test('Plan View', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`);
});
test('Resize Plan View @2p', async ({ browser, theme }) => {
// need to set viewport to null to allow for resizing
const newContext = await browser.newContext({
viewport: null
});
const newPage = await newContext.newPage();
await newPage.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
const plan = await createPlanFromJSON(newPage, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(newPage, examplePlanSmall2, plan.url);
// resize the window
await newPage.setViewportSize({ width: 800, height: 600 });
await percySnapshot(newPage, `Plan View resized (theme: ${theme})`);
});
test('Plan View w/ draft status', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test (Draft)',
json: examplePlanSmall2
});
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
await setDraftStatusForPlan(page, plan);
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`);
});
});
test.afterEach(async ({ page }, testInfo) => {
await scanForA11yViolations(page, testInfo.title);
});

View File

@ -26,7 +26,6 @@ export default class SinewaveLimitProvider extends EventEmitter {
#openmct;
#observingStaleness;
#watchingTheClock;
#isRealTime;
constructor(openmct) {
super();
@ -34,7 +33,6 @@ export default class SinewaveLimitProvider extends EventEmitter {
this.#openmct = openmct;
this.#observingStaleness = {};
this.#watchingTheClock = false;
this.#isRealTime = undefined;
}
supportsStaleness(domainObject) {
@ -61,10 +59,7 @@ export default class SinewaveLimitProvider extends EventEmitter {
subscribeToStaleness(domainObject, callback) {
const id = this.#getObjectKeyString(domainObject);
if (this.#isRealTime === undefined) {
this.#updateRealTime(this.#openmct.time.getMode());
}
this.#realTimeCheck();
this.#handleClockUpdate();
if (this.#observerExists(id)) {
@ -92,17 +87,15 @@ export default class SinewaveLimitProvider extends EventEmitter {
if (observers && !this.#watchingTheClock) {
this.#watchingTheClock = true;
this.#openmct.time.on('modeChanged', this.#updateRealTime, this);
this.#openmct.time.on('modeChanged', this.#realTimeCheck, this);
} else if (!observers && this.#watchingTheClock) {
this.#watchingTheClock = false;
this.#openmct.time.off('modeChanged', this.#updateRealTime, this);
this.#openmct.time.off('modeChanged', this.#realTimeCheck, this);
}
}
#updateRealTime(mode) {
this.#isRealTime = mode !== 'fixed';
if (!this.#isRealTime) {
#realTimeCheck() {
if (!this.#openmct.time.isRealTime()) {
Object.keys(this.#observingStaleness).forEach((id) => {
this.#updateStaleness(id, false);
});
@ -140,7 +133,7 @@ export default class SinewaveLimitProvider extends EventEmitter {
}
#providingStaleness(domainObject) {
return domainObject.telemetry?.staleness === true && this.#isRealTime;
return domainObject.telemetry?.staleness === true && this.#openmct.time.isRealTime();
}
#getObjectKeyString(object) {

235
package-lock.json generated
View File

@ -43,7 +43,7 @@
"eslint-plugin-unicorn": "49.0.0",
"eslint-plugin-vue": "9.22.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.13.0",
"eventemitter3": "1.2.0",
"eventemitter3": "5.0.1",
"file-saver": "2.0.5",
"flatbush": "4.2.0",
"git-rev-sync": "3.0.2",
@ -84,7 +84,7 @@
"tiny-emitter": "2.1.0",
"typescript": "5.3.3",
"uuid": "9.0.1",
"vue": "3.4.19",
"vue": "3.4.24",
"vue-eslint-parser": "9.4.2",
"vue-loader": "16.8.3",
"webpack": "5.90.3",
@ -104,7 +104,7 @@
"@axe-core/playwright": "4.8.5",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.42.1",
"@playwright/test": "1.44.0",
"@types/sinonjs__fake-timers": "8.1.5",
"sinon": "17.0.0"
}
@ -398,9 +398,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
"integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz",
"integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@ -1559,12 +1559,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
"integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz",
"integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==",
"dev": true,
"dependencies": {
"playwright": "1.42.1"
"playwright": "1.44.0"
},
"bin": {
"playwright": "cli.js"
@ -1996,103 +1996,103 @@
}
},
"node_modules/@vue/reactivity": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz",
"integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.24.tgz",
"integrity": "sha512-nup3fSYg4i4LtNvu9slF/HF/0dkMQYfepUdORBcMSsankzRPzE7ypAFurpwyRBfU1i7Dn1kcwpYsE1wETSh91g==",
"dev": true,
"dependencies": {
"@vue/shared": "3.4.19"
"@vue/shared": "3.4.24"
}
},
"node_modules/@vue/reactivity/node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==",
"dev": true
},
"node_modules/@vue/runtime-core": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz",
"integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.24.tgz",
"integrity": "sha512-c7iMfj6cJMeAG3s5yOn9Rc5D9e2/wIuaozmGf/ICGCY3KV5H7mbTVdvEkd4ZshTq7RUZqj2k7LMJWVx+EBiY1g==",
"dev": true,
"dependencies": {
"@vue/reactivity": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/reactivity": "3.4.24",
"@vue/shared": "3.4.24"
}
},
"node_modules/@vue/runtime-core/node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==",
"dev": true
},
"node_modules/@vue/runtime-dom": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz",
"integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.24.tgz",
"integrity": "sha512-uXKzuh/Emfad2Y7Qm0ABsLZZV6H3mAJ5ZVqmAOlrNQRf+T5mxpPGZBfec1hkP41t6h6FwF6RSGCs/gd8WbuySQ==",
"dev": true,
"dependencies": {
"@vue/runtime-core": "3.4.19",
"@vue/shared": "3.4.19",
"@vue/runtime-core": "3.4.24",
"@vue/shared": "3.4.24",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==",
"dev": true
},
"node_modules/@vue/server-renderer": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz",
"integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.24.tgz",
"integrity": "sha512-H+DLK4sQF6sRgzKyofmlEVBIV/9KrQU6HIV7nt6yIwSGGKvSwlV8pqJlebUKLpbXaNHugdSfAbP6YmXF69lxow==",
"dev": true,
"dependencies": {
"@vue/compiler-ssr": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-ssr": "3.4.24",
"@vue/shared": "3.4.24"
},
"peerDependencies": {
"vue": "3.4.19"
"vue": "3.4.24"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/compiler-core": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz",
"integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.24.tgz",
"integrity": "sha512-vbW/tgbwJYj62N/Ww99x0zhFTkZDTcGh3uwJEuadZ/nF9/xuFMC4693P9r+3sxGXISABpDKvffY5ApH9pmdd1A==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.23.9",
"@vue/shared": "3.4.19",
"@babel/parser": "^7.24.4",
"@vue/shared": "3.4.24",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/compiler-dom": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz",
"integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.24.tgz",
"integrity": "sha512-4XgABML/4cNndVsQndG6BbGN7+EoisDwi3oXNovqL/4jdNhwvP8/rfRMTb6FxkxIxUUtg6AI1/qZvwfSjxJiWA==",
"dev": true,
"dependencies": {
"@vue/compiler-core": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-core": "3.4.24",
"@vue/shared": "3.4.24"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/compiler-ssr": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz",
"integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.24.tgz",
"integrity": "sha512-ZsAtr4fhaUFnVcDqwW3bYCSDwq+9Gk69q2r/7dAHDrOMw41kylaMgOP4zRnn6GIEJkQznKgrMOGPMFnLB52RbQ==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-dom": "3.4.24",
"@vue/shared": "3.4.24"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==",
"dev": true
},
"node_modules/@vue/shared": {
@ -5391,9 +5391,9 @@
}
},
"node_modules/eventemitter3": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz",
"integrity": "sha512-DOFqA1MF46fmZl2xtzXR3MPCRsXqgoFqdXcrCVYM3JNnfUeHTm/fh/v/iU7gBFpwkuBmoJPAm5GuhdDfSEJMJA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true
},
"node_modules/events": {
@ -7778,15 +7778,12 @@
}
},
"node_modules/magic-string": {
"version": "0.30.8",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
"version": "0.30.10",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
"integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/make-dir": {
@ -8836,12 +8833,12 @@
}
},
"node_modules/playwright": {
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
"integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz",
"integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==",
"dev": true,
"dependencies": {
"playwright-core": "1.42.1"
"playwright-core": "1.44.0"
},
"bin": {
"playwright": "cli.js"
@ -8854,9 +8851,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
"integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz",
"integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@ -8901,9 +8898,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"dev": true,
"funding": [
{
@ -8922,7 +8919,7 @@
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -10366,9 +10363,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@ -11241,16 +11238,16 @@
"dev": true
},
"node_modules/vue": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz",
"integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.24.tgz",
"integrity": "sha512-NPdx7dLGyHmKHGRRU5bMRYVE+rechR+KDU5R2tSTNG36PuMwbfAJ+amEvOAw7BPfZp5sQulNELSLm5YUkau+Sg==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.4.19",
"@vue/compiler-sfc": "3.4.19",
"@vue/runtime-dom": "3.4.19",
"@vue/server-renderer": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-dom": "3.4.24",
"@vue/compiler-sfc": "3.4.24",
"@vue/runtime-dom": "3.4.24",
"@vue/server-renderer": "3.4.24",
"@vue/shared": "3.4.24"
},
"peerDependencies": {
"typescript": "*"
@ -11440,59 +11437,59 @@
}
},
"node_modules/vue/node_modules/@vue/compiler-core": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz",
"integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.24.tgz",
"integrity": "sha512-vbW/tgbwJYj62N/Ww99x0zhFTkZDTcGh3uwJEuadZ/nF9/xuFMC4693P9r+3sxGXISABpDKvffY5ApH9pmdd1A==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.23.9",
"@vue/shared": "3.4.19",
"@babel/parser": "^7.24.4",
"@vue/shared": "3.4.24",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
}
},
"node_modules/vue/node_modules/@vue/compiler-dom": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz",
"integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.24.tgz",
"integrity": "sha512-4XgABML/4cNndVsQndG6BbGN7+EoisDwi3oXNovqL/4jdNhwvP8/rfRMTb6FxkxIxUUtg6AI1/qZvwfSjxJiWA==",
"dev": true,
"dependencies": {
"@vue/compiler-core": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-core": "3.4.24",
"@vue/shared": "3.4.24"
}
},
"node_modules/vue/node_modules/@vue/compiler-sfc": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz",
"integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.24.tgz",
"integrity": "sha512-nRAlJUK02FTWfA2nuvNBAqsDZuERGFgxZ8sGH62XgFSvMxO2URblzulExsmj4gFZ8e+VAyDooU9oAoXfEDNxTA==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.23.9",
"@vue/compiler-core": "3.4.19",
"@vue/compiler-dom": "3.4.19",
"@vue/compiler-ssr": "3.4.19",
"@vue/shared": "3.4.19",
"@babel/parser": "^7.24.4",
"@vue/compiler-core": "3.4.24",
"@vue/compiler-dom": "3.4.24",
"@vue/compiler-ssr": "3.4.24",
"@vue/shared": "3.4.24",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.6",
"postcss": "^8.4.33",
"source-map-js": "^1.0.2"
"magic-string": "^0.30.10",
"postcss": "^8.4.38",
"source-map-js": "^1.2.0"
}
},
"node_modules/vue/node_modules/@vue/compiler-ssr": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz",
"integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.24.tgz",
"integrity": "sha512-ZsAtr4fhaUFnVcDqwW3bYCSDwq+9Gk69q2r/7dAHDrOMw41kylaMgOP4zRnn6GIEJkQznKgrMOGPMFnLB52RbQ==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-dom": "3.4.24",
"@vue/shared": "3.4.24"
}
},
"node_modules/vue/node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==",
"dev": true
},
"node_modules/watchpack": {

View File

@ -46,7 +46,7 @@
"eslint-plugin-unicorn": "49.0.0",
"eslint-plugin-vue": "9.22.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.13.0",
"eventemitter3": "1.2.0",
"eventemitter3": "5.0.1",
"file-saver": "2.0.5",
"flatbush": "4.2.0",
"git-rev-sync": "3.0.2",
@ -87,7 +87,7 @@
"tiny-emitter": "2.1.0",
"typescript": "5.3.3",
"uuid": "9.0.1",
"vue": "3.4.19",
"vue": "3.4.24",
"vue-eslint-parser": "9.4.2",
"vue-loader": "16.8.3",
"webpack": "5.90.3",
@ -96,7 +96,7 @@
"webpack-merge": "5.10.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./test-results ./.nyc_output",
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./e2e/test-results ./.nyc_output ./e2e/.nyc_output",
"start": "npx webpack serve --config ./.webpack/webpack.dev.mjs",
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.mjs",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.mjs",
@ -161,4 +161,4 @@
"keywords": [
"nasa"
]
}
}

View File

@ -79,7 +79,6 @@ import Browse from './ui/router/Browse.js';
export class MCT extends EventEmitter {
constructor() {
super();
EventEmitter.call(this);
this.buildInfo = {
version: __OPENMCT_VERSION__,
@ -371,6 +370,5 @@ export class MCT extends EventEmitter {
destroy() {
window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy');
this.router.destroy();
}
}

View File

@ -22,6 +22,10 @@
import TimeContext from './TimeContext.js';
/**
* @typedef {import('./TimeAPI').TimeConductorBounds} TimeConductorBounds
*/
/**
* The GlobalContext handles getting and setting time of the openmct application in general.
* Views will use this context unless they specify an alternate/independent time context
@ -38,12 +42,10 @@ class GlobalTimeContext extends TimeContext {
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @param {TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
* @returns {TimeConductorBounds}
* @override
*/
bounds(newBounds) {
if (arguments.length > 0) {
@ -61,9 +63,9 @@ class GlobalTimeContext extends TimeContext {
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
* @override
*/
tick(timestamp) {
super.tick.call(this, ...arguments);
@ -81,11 +83,8 @@ class GlobalTimeContext extends TimeContext {
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
timeOfInterest(newTOI) {
if (arguments.length > 0) {
@ -93,8 +92,7 @@ class GlobalTimeContext extends TimeContext {
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
* @property {number} timeOfInterest time of interest
*/
this.emit('timeOfInterest', this.toi);
}

View File

@ -23,19 +23,36 @@
import { MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants.js';
import TimeContext from './TimeContext.js';
/**
* @typedef {import('./TimeAPI.js').default} TimeAPI
* @typedef {import('./GlobalTimeContext.js').default} GlobalTimeContext
* @typedef {import('./TimeAPI.js').TimeSystem} TimeSystem
* @typedef {import('./TimeContext.js').Mode} Mode
* @typedef {import('./TimeContext.js').TimeConductorBounds} TimeConductorBounds
* @typedef {import('./TimeAPI.js').ClockOffsets} ClockOffsets
*/
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
/**
* @param {import('openmct').OpenMCT} openmct - The Open MCT application instance.
* @param {TimeAPI & GlobalTimeContext} globalTimeContext - The global time context.
* @param {import('openmct').ObjectPath} objectPath - The path of objects.
*/
constructor(openmct, globalTimeContext, objectPath) {
super();
/** @type {any} */
this.openmct = openmct;
/** @type {Function[]} */
this.unlisteners = [];
/** @type {TimeAPI & GlobalTimeContext | undefined} */
this.globalTimeContext = globalTimeContext;
// We always start with the global time context.
// This upstream context will be undefined when an independent time context is added later.
/** @type {TimeAPI & GlobalTimeContext | undefined} */
this.upstreamTimeContext = this.globalTimeContext;
/** @type {Array<any>} */
this.objectPath = objectPath;
this.refreshContext = this.refreshContext.bind(this);
this.resetContext = this.resetContext.bind(this);
@ -47,6 +64,10 @@ class IndependentTimeContext extends TimeContext {
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
}
/**
* @deprecated
* @override
*/
bounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
@ -55,6 +76,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
getBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getBounds();
@ -63,6 +87,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
setBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setBounds(...arguments);
@ -71,6 +98,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
tick() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
@ -79,6 +109,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
clockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
@ -87,6 +120,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
getClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClockOffsets();
@ -95,6 +131,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
setClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClockOffsets(...arguments);
@ -103,12 +142,24 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
*
* @param {number} newTOI
* @returns {number}
*/
timeOfInterest(newTOI) {
return this.globalTimeContext.timeOfInterest(...arguments);
}
/**
*
* @param {TimeSystem | string} timeSystemOrKey
* @param {TimeConductorBounds} bounds
* @returns {TimeSystem}
* @override
*/
timeSystem(timeSystemOrKey, bounds) {
return this.globalTimeContext.timeSystem(...arguments);
return this.globalTimeContext.setTimeSystem(...arguments);
}
/**
@ -116,6 +167,7 @@ class IndependentTimeContext extends TimeContext {
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
* @override
*/
getTimeSystem() {
return this.globalTimeContext.getTimeSystem();
@ -246,6 +298,7 @@ class IndependentTimeContext extends TimeContext {
/**
* Get the current mode.
* @return {Mode} the current mode;
* @override
*/
getMode() {
if (this.upstreamTimeContext) {
@ -259,9 +312,8 @@ class IndependentTimeContext extends TimeContext {
* 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;
* @param {TimeConductorBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @return {Mode | undefined} the currently active mode;
*/
setMode(mode, offsetsOrBounds) {
if (!mode) {
@ -299,6 +351,10 @@ class IndependentTimeContext extends TimeContext {
return this.mode;
}
/**
* @returns {boolean}
* @override
*/
isRealTime() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.isRealTime(...arguments);
@ -307,6 +363,10 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @returns {number}
* @override
*/
now() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.now(...arguments);
@ -343,6 +403,9 @@ class IndependentTimeContext extends TimeContext {
this.unlisteners = [];
}
/**
* Reset the time context to the global time context
*/
resetContext() {
if (this.upstreamTimeContext) {
this.stopFollowingTimeContext();
@ -352,6 +415,7 @@ class IndependentTimeContext extends TimeContext {
/**
* Refresh the time context, following any upstream time contexts as necessary
* @param {string} [viewKey] The key of the view to refresh
*/
refreshContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
@ -366,14 +430,21 @@ 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());
}
/**
* @returns {boolean} True if this time context has an independent context, false otherwise
*/
hasOwnContext() {
return this.upstreamTimeContext === undefined;
}
/**
* Get the upstream time context of this time context
* @returns {TimeAPI & GlobalTimeContext | undefined} The upstream time context
*/
getUpstreamContext() {
// If a view has an independent context, don't return an upstream context
// Be aware that when a new independent time context is created, we assign the global context as default

View File

@ -25,6 +25,41 @@ import IndependentTimeContext from '@/api/time/IndependentTimeContext';
import GlobalTimeContext from './GlobalTimeContext.js';
/**
* @typedef {import('./TimeContext.js').default} TimeContext
*/
/**
* @typedef {import('./TimeContext.js').TimeConductorBounds} TimeConductorBounds
*/
/**
* @typedef {import('./TimeContext.js').ClockOffsets} ClockOffsets
*/
/**
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
* MCT supports multiple different types of time values, although all are
* intrinsically represented by numbers, the meaning of those numbers can
* differ depending on context.
*
* A default time system is provided by Open MCT in the form of the {@link UTCTimeSystem},
* which represents integer values as ms in the Unix epoch. An example of
* another time system might be "sols" for a Martian mission. TimeSystems do
* not address the issue of converting between time systems.
*
* @typedef {Object} TimeSystem
* @property {string} key A unique identifier
* @property {string} name A human-readable descriptor
* @property {string} [cssClass] Specify a css class defining an icon for
* this time system. This will be visible next to the time system in the
* menu in the Time Conductor
* @property {string} timeFormat The key of a format to use when displaying
* discrete timestamps from this time system
* @property {string} [durationFormat] The key of a format to use when
* displaying a duration or relative span of time in this time system.
*/
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
@ -41,8 +76,8 @@ import GlobalTimeContext from './GlobalTimeContext.js';
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
* @class
* @extends {GlobalTimeContext}
*/
class TimeAPI extends GlobalTimeContext {
constructor(openmct) {
@ -51,33 +86,9 @@ class TimeAPI extends GlobalTimeContext {
this.independentContexts = new Map();
}
/**
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
* MCT supports multiple different types of time values, although all are
* intrinsically represented by numbers, the meaning of those numbers can
* differ depending on context.
*
* A default time system is provided by Open MCT in the form of the {@link UTCTimeSystem},
* which represents integer values as ms in the Unix epoch. An example of
* another time system might be "sols" for a Martian mission. TimeSystems do
* not address the issue of converting between time systems.
*
* @typedef {Object} TimeSystem
* @property {string} key A unique identifier
* @property {string} name A human-readable descriptor
* @property {string} [cssClass] Specify a css class defining an icon for
* this time system. This will be visible next to the time system in the
* menu in the Time Conductor
* @property {string} timeFormat The key of a format to use when displaying
* discrete timestamps from this time system
* @property {string} [durationFormat] The key of a format to use when
* displaying a duration or relative span of time in this time system.
*/
/**
* Register a new time system. Once registered it can activated using
* {@link TimeAPI.timeSystem}, and can be referenced via its key in [Time Conductor configuration](@link https://github.com/nasa/openmct/blob/master/API.md#time-conductor).
* @memberof module:openmct.TimeAPI#
* @param {TimeSystem} timeSystem A time system object.
*/
addTimeSystem(timeSystem) {
@ -109,7 +120,6 @@ class TimeAPI extends GlobalTimeContext {
/**
* Register a new Clock.
* @memberof module:openmct.TimeAPI#
* @param {Clock} clock
*/
addClock(clock) {
@ -117,9 +127,7 @@ class TimeAPI extends GlobalTimeContext {
}
/**
* @memberof module:openmct.TimeAPI#
* @returns {Clock[]}
* @memberof module:openmct.TimeAPI#
*/
getAllClocks() {
return Array.from(this.clocks.values());
@ -128,11 +136,9 @@ class TimeAPI extends GlobalTimeContext {
/**
* Get or set an independent time context which follows the TimeAPI timeSystem,
* but with different offsets for a given domain object
* @param {key | string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeConductorBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {key | string} clockKey the real time clock key currently in use
* @memberof module:openmct.TimeAPI#
* @method addIndependentTimeContext
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
@ -159,9 +165,8 @@ class TimeAPI extends GlobalTimeContext {
/**
* Get the independent time context which follows the TimeAPI timeSystem,
* but with different offsets.
* @param {key | string} key The identifier key of the domain object these offsets
* @memberof module:openmct.TimeAPI#
* @method getIndependentTimeContext
* @param {string} key The identifier key of the domain object these offsets
* @returns {IndependentTimeContext} The independent time context
*/
getIndependentContext(key) {
return this.independentContexts.get(key);
@ -170,9 +175,8 @@ class TimeAPI extends GlobalTimeContext {
/**
* Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
* Otherwise, the global time context will be returned.
* @param { Array } objectPath The view's objectPath
* @memberof module:openmct.TimeAPI#
* @method getContextForView
* @param {Array} objectPath The view's objectPath
* @returns {TimeContext | GlobalTimeContext} The time context
*/
getContextForView(objectPath) {
if (!objectPath || !Array.isArray(objectPath)) {

View File

@ -57,7 +57,7 @@ describe('The Time API', function () {
expect(api.timeOfInterest()).toBe(toi);
});
it('Allows setting of valid bounds', function () {
it('[Legacy TimeAPI]: Allows setting of valid bounds', function () {
bounds = {
start: 0,
end: 1
@ -67,7 +67,17 @@ describe('The Time API', function () {
expect(api.bounds()).toEqual(bounds);
});
it('Disallows setting of invalid bounds', function () {
it('Allows setting of valid bounds', function () {
bounds = {
start: 0,
end: 1
};
expect(api.getBounds()).not.toBe(bounds);
expect(api.setBounds.bind(api, bounds)).not.toThrow();
expect(api.getBounds()).toEqual(bounds);
});
it('[Legacy TimeAPI]: Disallows setting of invalid bounds', function () {
bounds = {
start: 1,
end: 0
@ -82,7 +92,22 @@ describe('The Time API', function () {
expect(api.bounds()).not.toEqual(bounds);
});
it('Allows setting of previously registered time system with bounds', function () {
it('Disallows setting of invalid bounds', function () {
bounds = {
start: 1,
end: 0
};
expect(api.getBounds()).not.toEqual(bounds);
expect(api.setBounds.bind(api, bounds)).toThrow();
expect(api.getBounds()).not.toEqual(bounds);
bounds = { start: 1 };
expect(api.getBounds()).not.toEqual(bounds);
expect(api.setBounds.bind(api, bounds)).toThrow();
expect(api.getBounds()).not.toEqual(bounds);
});
it('[Legacy TimeAPI]: Allows setting of previously registered time system with bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
@ -91,7 +116,16 @@ describe('The Time API', function () {
expect(api.timeSystem()).toEqual(timeSystem);
});
it('Disallows setting of time system without bounds', function () {
it('Allows setting of previously registered time system with bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.getTimeSystem()).not.toBe(timeSystem);
expect(function () {
api.setTimeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.getTimeSystem()).toEqual(timeSystem);
});
it('[Legacy TimeAPI]: Disallows setting of time system without bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
@ -100,6 +134,32 @@ describe('The Time API', function () {
expect(api.timeSystem()).not.toBe(timeSystem);
});
it('Allows setting of time system without bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.getTimeSystem()).not.toBe(timeSystem);
expect(function () {
api.setTimeSystem(timeSystemKey);
}).not.toThrow();
expect(api.getTimeSystem()).not.toBe(timeSystem);
});
it('Disallows setting of invalid time system', function () {
expect(function () {
api.setTimeSystem();
}).toThrow();
expect(function () {
api.setTimeSystem('invalidTimeSystemKey');
}).toThrow();
expect(function () {
api.setTimeSystem({
key: 'invalidTimeSystemKey'
});
}).toThrow();
expect(function () {
api.setTimeSystem(42);
}).toThrow();
});
it('allows setting of timesystem without bounds with clock', function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
@ -114,7 +174,7 @@ describe('The Time API', function () {
expect(api.timeSystem()).toEqual(timeSystem);
});
it('Emits an event when time system changes', function () {
it('Emits a legacy event when time system changes', function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on('timeSystem', eventListener);
@ -122,6 +182,14 @@ describe('The Time API', function () {
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it('Emits an event when time system changes', function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on('timeSystemChanged', eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it('Emits an event when time of interest changes', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('timeOfInterest', eventListener);
@ -129,13 +197,20 @@ describe('The Time API', function () {
expect(eventListener).toHaveBeenCalledWith(toi);
});
it('Emits an event when bounds change', function () {
it('Emits a legacy event when bounds change', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('bounds', eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it('Emits an event when bounds change', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('boundsChanged', eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it('If bounds are set and TOI lies inside them, do not change TOI', function () {
api.timeOfInterest(6);
api.bounds({
@ -154,13 +229,39 @@ describe('The Time API', function () {
expect(api.timeOfInterest()).toBeUndefined();
});
it('Maintains delta during tick', function () {});
it('Maintains delta during tick', function () {
const initialBounds = { start: 100, end: 200 };
api.bounds(initialBounds);
const mockTickSource = jasmine.createSpyObj('mockTickSource', ['on', 'off', 'currentValue']);
mockTickSource.key = 'mct';
mockTickSource.currentValue.and.returnValue(150);
api.addClock(mockTickSource);
api.clock('mct', { start: 0, end: 100 });
it('Allows registered time system to be activated', function () {});
// Simulate a tick event
const tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(150);
const newBounds = api.bounds();
expect(newBounds.end - newBounds.start).toEqual(initialBounds.end - initialBounds.start);
});
it('Allows registered time system to be activated', function () {
api.addClock(clock);
api.clock(clockKey, { start: 0, end: 100 });
api.addTimeSystem(timeSystem);
api.timeSystem(timeSystemKey);
expect(api.timeSystem().key).toEqual(timeSystemKey);
});
it('Allows a registered tick source to be activated', function () {
const mockTickSource = jasmine.createSpyObj('mockTickSource', ['on', 'off', 'currentValue']);
mockTickSource.key = 'mockTickSource';
mockTickSource.currentValue.and.returnValue(50);
api.addClock(mockTickSource);
api.clock(mockTickSource.key, { start: 0, end: 100 });
expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
describe(' when enabling a tick source', function () {
@ -184,7 +285,7 @@ describe('The Time API', function () {
api.addClock(anotherMockTickSource);
});
it('sets bounds based on current value', function () {
it('[Legacy TimeAPI]: sets bounds based on current value', function () {
api.clock('mts', mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
@ -192,23 +293,46 @@ describe('The Time API', function () {
});
});
it('a new tick listener is registered', function () {
it('does not set bounds based on current value', function () {
api.setClock('mts');
expect(api.getBounds()).toEqual({});
});
it('does not set invalid clock', function () {
expect(function () {
api.setClock();
}).toThrow();
expect(function () {
api.setClock({});
}).toThrow();
expect(function () {
api.setClock('invalidClockKey');
}).toThrow();
});
it('[Legacy TimeAPI]: a new tick listener is registered', function () {
api.clock('mts', mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
it('a new tick listener is registered', function () {
api.setClock('mts', mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
it('listener of existing tick source is reregistered', function () {
api.clock('mts', mockOffsets);
api.clock('amts', mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
xit('Allows the active clock to be set and unset', function () {
it('[Legacy TimeAPI]: 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();
// Unset the clock
api.stopClock();
expect(api.clock()).toBeUndefined();
});
it('Provides a default time context', () => {

View File

@ -20,26 +20,89 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import EventEmitter from 'eventemitter3';
import { FIXED_MODE_KEY, MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants.js';
/**
* @typedef {import('../../utils/clock/DefaultClock.js').default} Clock
*/
/**
* @typedef {import('./TimeAPI.js').TimeSystem} TimeSystem
*/
/**
* @typedef {Object} TimeConductorBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {Object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* @typedef {Object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* @typedef {'fixed' | 'realtime'} Mode The time conductor mode.
*/
/**
* @class TimeContext
* @extends EventEmitter
*/
class TimeContext extends EventEmitter {
constructor() {
super();
//The Time System
/**
* The time systems available to the TimeAPI.
* @type {Map<string, TimeSystem>}
*/
this.timeSystems = new Map();
/**
* The currently applied time system.
* @type {TimeSystem | undefined}
*/
this.system = undefined;
/**
* The clocks available to the TimeAPI.
* @type {Map<string, import('../../utils/clock/DefaultClock.js').default>}
*/
this.clocks = new Map();
/**
* The current bounds of the time conductor.
* @type {TimeConductorBounds}
*/
this.boundsVal = {
start: undefined,
end: undefined
};
/**
* The currently active clock.
* @type {Clock | undefined}
*/
this.activeClock = undefined;
this.offsets = undefined;
this.mode = undefined;
@ -51,11 +114,9 @@ class TimeContext extends EventEmitter {
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @param {TimeConductorBounds} bounds
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
* @deprecated This method is deprecated. Use "getTimeSystem" and "setTimeSystem" instead.
*/
timeSystem(timeSystemOrKey, bounds) {
this.#warnMethodDeprecated('"timeSystem"', '"getTimeSystem" and "setTimeSystem"');
@ -101,11 +162,8 @@ class TimeContext extends EventEmitter {
* 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
* */
* @type {TimeSystem}
*/
const system = this.#copy(this.system);
this.emit('timeSystem', system);
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, system);
@ -118,21 +176,11 @@ class TimeContext extends EventEmitter {
return this.system;
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {Object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @param {TimeConductorBounds} bounds The start and end time of the conductor.
* @returns {ValidationResult} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
validateBounds(bounds) {
if (
@ -162,12 +210,10 @@ class TimeContext extends EventEmitter {
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @param {TimeConductorBounds} [newBounds] The new bounds to set. If not provided, current bounds will be returned.
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
* @returns {TimeConductorBounds} The current bounds of the time conductor.
* @deprecated This method is deprecated. Use "getBounds" and "setBounds" instead.
*/
bounds(newBounds) {
this.#warnMethodDeprecated('"bounds"', '"getBounds" and "setBounds"');
@ -183,7 +229,6 @@ class TimeContext extends EventEmitter {
/**
* 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 (ie. was an automatic update), false otherwise.
@ -200,9 +245,7 @@ class TimeContext extends EventEmitter {
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns { ValidationResult } A validation error, and true/false if valid or not
* @memberof module:openmct.TimeAPI#
* @method validateOffsets
* @returns {ValidationResult} A validation error, and true/false if valid or not
*/
validateOffsets(offsets) {
if (
@ -228,34 +271,13 @@ class TimeContext extends EventEmitter {
};
}
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {Object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or 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}
* @param {ClockOffsets} [offsets] The new clock offsets to set. If not provided, current offsets will be returned.
* @returns {ClockOffsets} The current clock offsets.
* @deprecated This method is deprecated. Use "getClockOffsets" and "setClockOffsets" instead.
*/
clockOffsets(offsets) {
this.#warnMethodDeprecated('"clockOffsets"', '"getClockOffsets" and "setClockOffsets"');
@ -293,6 +315,7 @@ class TimeContext extends EventEmitter {
* Stop following the currently active clock. This will
* revert all views to showing a static time frame defined by the current
* bounds.
* @deprecated This method is deprecated.
*/
stopClock() {
this.#warnMethodDeprecated('"stopClock"');
@ -304,12 +327,14 @@ class TimeContext extends EventEmitter {
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {string|Clock} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
* (Legacy) Emits a "clock" event with the new clock.
* Emits a "clockChanged" event with the new clock.
* @return {Clock|undefined} the currently active clock; undefined if in fixed mode
* @deprecated This method is deprecated. Use "getClock" and "setClock" instead.
*/
clock(keyOrClock, offsets) {
this.#warnMethodDeprecated('"clock"', '"getClock" and "setClock"');
@ -339,7 +364,6 @@ class TimeContext extends EventEmitter {
/**
* 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
*/
@ -361,7 +385,7 @@ class TimeContext extends EventEmitter {
}
/**
* Update bounds based on provided time and current offsets
* Update bounds based on provided time and current offsets.
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
@ -385,8 +409,6 @@ class TimeContext extends EventEmitter {
/**
* Get the timestamp of the current clock
* @returns {number} current timestamp of current clock regardless of mode
* @memberof module:openmct.TimeAPI#
* @method now
*/
now() {
@ -396,8 +418,6 @@ class TimeContext extends EventEmitter {
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.system;
@ -405,12 +425,9 @@ class TimeContext extends EventEmitter {
/**
* 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
* Emits a "timeSystem" event with the new time system.
* @param {TimeSystem | string} timeSystemOrKey The time system to set, or its key
* @param {TimeConductorBounds} [bounds] Optional bounds to set
*/
setTimeSystem(timeSystemOrKey, bounds) {
if (timeSystemOrKey === undefined) {
@ -441,7 +458,6 @@ class TimeContext extends EventEmitter {
* 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
* */
@ -456,9 +472,7 @@ class TimeContext extends EventEmitter {
/**
* 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
* @returns {TimeConductorBounds} The current bounds of the time conductor.
*/
getBounds() {
//Return a copy to prevent direct mutation of time conductor bounds.
@ -469,12 +483,8 @@ class TimeContext extends EventEmitter {
* 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
* @param {TimeConductorBounds} newBounds The new bounds to set.
* @throws {Error} Validation error if bounds are invalid
*/
setBounds(newBounds) {
const validationResult = this.validateBounds(newBounds);
@ -487,7 +497,6 @@ class TimeContext extends EventEmitter {
/**
* 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.
@ -498,7 +507,7 @@ class TimeContext extends EventEmitter {
/**
* Get the active clock.
* @return {Clock} the currently active clock;
* @return {Clock|undefined} the currently active clock; undefined if in fixed mode.
*/
getClock() {
return this.activeClock;
@ -509,9 +518,7 @@ class TimeContext extends EventEmitter {
* 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;
* @param {string|Clock} keyOrClock The clock to activate, or its key
*/
setClock(keyOrClock) {
let clock;
@ -540,7 +547,7 @@ class TimeContext extends EventEmitter {
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* @property {TimeContext} 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);
@ -549,7 +556,7 @@ class TimeContext extends EventEmitter {
/**
* Get the current mode.
* @return {Mode} the current mode;
* @return {Mode} the current mode
*/
getMode() {
return this.mode;
@ -559,9 +566,9 @@ class TimeContext extends EventEmitter {
* 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
* @param {TimeConductorBounds|ClockOffsets} offsetsOrBounds A time window of a fixed width
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
* @return {Mode | undefined} the currently active mode
*/
setMode(mode, offsetsOrBounds) {
if (!mode) {
@ -577,7 +584,6 @@ class TimeContext extends EventEmitter {
/**
* 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));
@ -610,18 +616,15 @@ class TimeContext extends EventEmitter {
/**
* Get the currently applied clock offsets.
* @returns {ClockOffsets}
* @returns {ClockOffsets} The current clock offsets.
*/
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}
* Set the currently applied clock offsets.
* @param {ClockOffsets} offsets The new clock offsets to set.
*/
setClockOffsets(offsets) {
const validationResult = this.validateOffsets(offsets);
@ -642,13 +645,20 @@ class TimeContext extends EventEmitter {
/**
* 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));
}
/**
* Prints a warning to the console when a deprecated method is used. Limits
* the number of times a warning is printed per unique method and newMethod
* combination.
* @param {string} method the deprecated method
* @param {string} [newMethod] the new method to use instead
* @returns
*/
#warnMethodDeprecated(method, newMethod) {
const MAX_CALLS = 1; // Only warn once per unique method and newMethod combination
@ -673,6 +683,11 @@ class TimeContext extends EventEmitter {
console.warn(message);
}
/**
* Deep copy an object.
* @param {object} object The object to copy
* @returns {object} The copied object
*/
#copy(object) {
return JSON.parse(JSON.stringify(object));
}

View File

@ -57,14 +57,21 @@
</template>
<script>
const CONTEXT_MENU_ACTIONS = ['viewDatumAction', 'viewHistoricalData', 'remove'];
const BLANK_VALUE = '---';
import { objectPathToUrl } from '/src/tools/url.js';
import PreviewAction from '@/ui/preview/PreviewAction.js';
import { REMOVE_ACTION_KEY } from '@/plugins/remove/RemoveAction.js';
import { VIEW_DATUM_ACTION_KEY } from '@/plugins/viewDatumAction/ViewDatumAction.js';
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
import { VIEW_HISTORICAL_DATA_ACTION_KEY } from '@/ui/preview/ViewHistoricalDataAction.js';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
const BLANK_VALUE = '---';
const CONTEXT_MENU_ACTIONS = [
VIEW_DATUM_ACTION_KEY,
VIEW_HISTORICAL_DATA_ACTION_KEY,
REMOVE_ACTION_KEY
];
export default {
mixins: [tooltipHelpers],
inject: ['openmct', 'currentView', 'renderWhenVisible'],
@ -212,7 +219,7 @@ export default {
this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.timestampKey = this.openmct.time.timeSystem().key;
this.timestampKey = this.openmct.time.getTimeSystem().key;
this.valueMetadata = undefined;
@ -236,14 +243,12 @@ export default {
this.setUnit();
}
this.previewAction = new PreviewAction(this.openmct);
this.previewAction.on('isVisible', this.togglePreviewState);
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
},
unmounted() {
this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.resetValues);
this.previewAction.off('isVisible', this.togglePreviewState);
this.telemetryCollection.destroy();
},

View File

@ -61,7 +61,7 @@ export default class URLTimeSettingsSynchronizer {
TIME_EVENTS.forEach((event) => {
this.openmct.time.on(event, this.setUrlFromTimeApi);
});
this.openmct.time.on('bounds', this.updateBounds);
this.openmct.time.on('boundsChanged', this.updateBounds);
}
destroy() {
@ -73,7 +73,7 @@ export default class URLTimeSettingsSynchronizer {
TIME_EVENTS.forEach((event) => {
this.openmct.time.off(event, this.setUrlFromTimeApi);
});
this.openmct.time.off('bounds', this.updateBounds);
this.openmct.time.off('boundsChanged', this.updateBounds);
}
updateTimeSettings() {

View File

@ -115,11 +115,11 @@ export default {
this.followTimeContext();
},
followTimeContext() {
this.timeContext.on('bounds', this.refreshData);
this.timeContext.on('boundsChanged', this.refreshData);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.refreshData);
this.timeContext.off('boundsChanged', this.refreshData);
}
},
addToComposition(telemetryObject) {
@ -253,7 +253,7 @@ export default {
};
},
getOptions() {
const { start, end } = this.timeContext.bounds();
const { start, end } = this.timeContext.getBounds();
return {
end,
@ -372,13 +372,13 @@ export default {
this.setTrace(key, telemetryObject.name, axisMetadata, xValues, yValues);
},
isDataInTimeRange(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key;
const timeSystemKey = this.timeContext.getTimeSystem().key;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValue = metadata.value(timeSystemKey) || { key: timeSystemKey };
let currentTimestamp = this.parse(key, metadataValue.key, datum);
return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp;
return currentTimestamp && this.timeContext.getBounds().end >= currentTimestamp;
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];

View File

@ -105,11 +105,11 @@ export default {
this.followTimeContext();
},
followTimeContext() {
this.timeContext.on('bounds', this.reloadTelemetryOnBoundsChange);
this.timeContext.on('boundsChanged', this.reloadTelemetryOnBoundsChange);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.reloadTelemetryOnBoundsChange);
this.timeContext.off('boundsChanged', this.reloadTelemetryOnBoundsChange);
}
},
addToComposition(telemetryObject) {
@ -306,7 +306,7 @@ export default {
this.trace = [trace];
},
getTimestampForDatum(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key;
const timeSystemKey = this.timeContext.getTimeSystem().key;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValue = metadata.value(timeSystemKey) || { format: timeSystemKey };
@ -327,7 +327,7 @@ export default {
return formats[metadataKey].parse(datum);
},
getOptions() {
const { start, end } = this.timeContext.bounds();
const { start, end } = this.timeContext.getBounds();
return {
end,

View File

@ -32,16 +32,18 @@ function inSelectionPath(openmct, domainObject) {
});
}
export default class ClearDataAction {
const CLEAR_DATA_ACTION_KEY = 'clear-data-action';
class ClearDataAction {
constructor(openmct, appliesToObjects) {
this.name = 'Clear Data for Object';
this.key = 'clear-data-action';
this.key = CLEAR_DATA_ACTION_KEY;
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
this.cssClass = 'icon-clear-data';
this._openmct = openmct;
this._appliesToObjects = appliesToObjects;
}
invoke(objectPath) {
let domainObject = null;
if (objectPath) {
@ -76,3 +78,7 @@ export default class ClearDataAction {
}
}
}
export { CLEAR_DATA_ACTION_KEY };
export default ClearDataAction;

View File

@ -245,7 +245,7 @@ export default class Condition extends EventEmitter {
latestTimestamp,
updatedCriterion.data,
this.timeSystems,
this.openmct.time.timeSystem()
this.openmct.time.getTimeSystem()
);
this.conditionManager.updateCurrentCondition(latestTimestamp);
}
@ -309,7 +309,7 @@ export default class Condition extends EventEmitter {
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
this.openmct.time.getTimeSystem()
);
});

View File

@ -46,14 +46,6 @@ export default class ConditionManager extends EventEmitter {
applied: false
};
this.initialize();
this.stopObservingForChanges = this.openmct.objects.observe(
this.conditionSetDomainObject,
'*',
(newDomainObject) => {
this.conditionSetDomainObject = newDomainObject;
}
);
}
async requestLatestValue(endpoint) {
@ -113,7 +105,7 @@ export default class ConditionManager extends EventEmitter {
{},
{},
this.timeSystems,
this.openmct.time.timeSystem()
this.openmct.time.getTimeSystem()
);
this.updateConditionResults({ id: id });
this.updateCurrentCondition(latestTimestamp);
@ -383,7 +375,7 @@ export default class ConditionManager extends EventEmitter {
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
this.openmct.time.getTimeSystem()
);
});
@ -518,10 +510,6 @@ export default class ConditionManager extends EventEmitter {
Object.values(this.subscriptions).forEach((unsubscribe) => unsubscribe());
delete this.subscriptions;
if (this.stopObservingForChanges) {
this.stopObservingForChanges();
}
this.conditions.forEach((condition) => {
condition.destroy();
});

View File

@ -41,7 +41,7 @@ export default class StyleRuleManager extends EventEmitter {
});
this.initialize(styleConfigurationWithNoSelection);
if (styleConfiguration.conditionSetIdentifier) {
this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('boundsChanged', this.refreshData);
this.subscribeToConditionSet();
} else {
this.applyStaticStyle();
@ -216,7 +216,7 @@ export default class StyleRuleManager extends EventEmitter {
}
if (!skipEventListeners) {
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('boundsChanged', this.refreshData);
this.openmct.editor.off('isEditing', this.toggleSubscription);
}

View File

@ -21,7 +21,11 @@
-->
<template>
<section id="conditionCollection" :class="{ 'is-expanded': expanded }">
<section
id="conditionCollection"
:class="{ 'is-expanded': expanded }"
aria-label="Condition Set Condition Collection"
>
<div class="c-cs__header c-section__header">
<span
class="c-disclosure-triangle c-tree__item__view-control is-enabled"

View File

@ -24,6 +24,7 @@
<div
class="c-condition-h"
:class="{ 'is-drag-target': draggingOver }"
aria-label="Condition Set Condition"
@dragover.prevent
@drop.prevent="dropCondition($event, conditionIndex)"
@dragenter="dragEnter($event, conditionIndex)"
@ -83,6 +84,7 @@
<input
v-model="condition.configuration.name"
class="t-condition-input__name"
aria-label="Condition Name Input"
type="text"
@change="persist"
/>
@ -91,7 +93,11 @@
<span class="c-cdef__label">Output</span>
<span class="c-cdef__controls">
<span class="c-cdef__control">
<select v-model="selectedOutputSelection" @change="setOutputValue">
<select
v-model="selectedOutputSelection"
aria-label="Condition Output Type"
@change="setOutputValue"
>
<option v-for="option in outputOptions" :key="option" :value="option">
{{ initCap(option) }}
</option>
@ -101,6 +107,7 @@
<input
v-if="selectedOutputSelection === outputOptions[2]"
v-model="condition.configuration.output"
aria-label="Condition Output String"
class="t-condition-name-input"
type="text"
@change="persist"

View File

@ -21,7 +21,7 @@
-->
<template>
<div class="c-cs" :class="{ 'is-stale': isStale }">
<div class="c-cs" :class="{ 'is-stale': isStale }" aria-label="Condition Set">
<section class="c-cs__current-output c-section">
<div class="c-cs__content c-cs__current-output-value">
<span class="c-cs__current-output-value__label">Current Output</span>

View File

@ -21,7 +21,12 @@
-->
<template>
<section v-show="isEditing" id="test-data" :class="{ 'is-expanded': expanded }">
<section
v-show="isEditing"
id="test-data"
:class="{ 'is-expanded': expanded }"
aria-label="Condition Set Test Data"
>
<div class="c-cs__header c-section__header">
<span
class="c-disclosure-triangle c-tree__item__view-control is-enabled"

View File

@ -142,7 +142,7 @@ import {
getConditionSetIdentifierForItem,
getConsolidatedStyleValues
} from '@/plugins/condition/utils/styleUtils';
import PreviewAction from '@/ui/preview/PreviewAction.js';
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
import FontStyleEditor from '../../../inspectorViews/styles/FontStyleEditor.vue';
import StyleEditor from './StyleEditor.vue';
@ -237,7 +237,7 @@ export default {
this.stylesManager.off('styleSelected', this.applyStyleToSelection);
},
mounted() {
this.previewAction = new PreviewAction(this.openmct);
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
this.isMultipleSelection = this.selection.length > 1;
this.getObjectsAndItemsFromSelection();
this.useConditionSetOutputAsLabel = this.getConfigurationForLabel();

View File

@ -227,7 +227,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
return Promise.all(telemetryRequests).then((telemetryRequestsResults) => {
let latestTimestamp;
const timeSystems = this.openmct.time.getAllTimeSystems();
const timeSystem = this.openmct.time.timeSystem();
const timeSystem = this.openmct.time.getTimeSystem();
telemetryRequestsResults.forEach((results, index) => {
const latestDatum =

View File

@ -1,13 +1,15 @@
import clipboard from '@/utils/clipboard';
export default class CopyToClipboardAction {
const COPY_TO_CLIPBOARD_ACTION_KEY = 'copyToClipboard';
class CopyToClipboardAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-duplicate';
this.description = 'Copy value to clipboard';
this.group = 'action';
this.key = 'copyToClipboard';
this.key = COPY_TO_CLIPBOARD_ACTION_KEY;
this.name = 'Copy to Clipboard';
this.priority = 1;
}
@ -36,3 +38,7 @@ export default class CopyToClipboardAction {
return row.formattedValueForCopy && typeof row.formattedValueForCopy === 'function';
}
}
export { COPY_TO_CLIPBOARD_ACTION_KEY };
export default CopyToClipboardAction;

View File

@ -72,11 +72,14 @@
</template>
<script>
import { COPY_TO_CLIPBOARD_ACTION_KEY } from '@/plugins/displayLayout/actions/CopyToClipboardAction.js';
import { COPY_TO_NOTEBOOK_ACTION_KEY } from '@/plugins/notebook/actions/CopyToNotebookAction.js';
import {
getDefaultNotebook,
getNotebookSectionAndPage
} from '@/plugins/notebook/utils/notebook-storage.js';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import { VIEW_HISTORICAL_DATA_ACTION_KEY } from '@/ui/preview/ViewHistoricalDataAction.js';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
import conditionalStylesMixin from '../mixins/objectStyles-mixin.js';
@ -84,7 +87,11 @@ import LayoutFrame from './LayoutFrame.vue';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1];
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
const CONTEXT_MENU_ACTIONS = [
COPY_TO_CLIPBOARD_ACTION_KEY,
COPY_TO_NOTEBOOK_ACTION_KEY,
VIEW_HISTORICAL_DATA_ACTION_KEY
];
export default {
makeDefinition(openmct, gridSize, domainObject, position) {
@ -280,7 +287,7 @@ export default {
await this.$nextTick();
},
formattedValueForCopy() {
const timeFormatterKey = this.openmct.time.timeSystem().key;
const timeFormatterKey = this.openmct.time.getTimeSystem().key;
const timeFormatter = this.formats[timeFormatterKey];
const unit = this.unit ? ` ${this.unit}` : '';
@ -381,7 +388,7 @@ export default {
return CONTEXT_MENU_ACTIONS.map((actionKey) => {
const action = this.openmct.actions.getAction(actionKey);
if (action.key === 'copyToNotebook') {
if (action.key === COPY_TO_NOTEBOOK_ACTION_KEY) {
action.name = defaultNotebookName;
}

View File

@ -18,6 +18,7 @@
}
&__value {
@include telemetryView();
@include isLimit();
}

View File

@ -21,10 +21,12 @@
*****************************************************************************/
import DuplicateTask from './DuplicateTask.js';
export default class DuplicateAction {
const DUPLICATE_ACTION_KEY = 'duplicate';
class DuplicateAction {
constructor(openmct) {
this.name = 'Duplicate';
this.key = 'duplicate';
this.key = DUPLICATE_ACTION_KEY;
this.description = 'Duplicate this object.';
this.cssClass = 'icon-duplicate';
this.group = 'action';
@ -169,3 +171,7 @@ export default class DuplicateAction {
this.transaction = null;
}
}
export { DUPLICATE_ACTION_KEY };
export default DuplicateAction;

View File

@ -22,8 +22,9 @@
import { v4 as uuid } from 'uuid';
import JSONExporter from '/src/exporters/JSONExporter.js';
const EXPORT_AS_JSON_ACTION_KEY = 'export.JSON';
export default class ExportAsJSONAction {
class ExportAsJSONAction {
#openmct;
/**
@ -39,7 +40,7 @@ export default class ExportAsJSONAction {
this.saveAs = this.saveAs.bind(this);
this.name = 'Export as JSON';
this.key = 'export.JSON';
this.key = EXPORT_AS_JSON_ACTION_KEY;
this.description = '';
this.cssClass = 'icon-export';
this.group = 'export';
@ -410,3 +411,7 @@ export default class ExportAsJSONAction {
return JSON.parse(JSON.stringify(object));
}
}
export { EXPORT_AS_JSON_ACTION_KEY };
export default ExportAsJSONAction;

View File

@ -45,7 +45,7 @@
@object-drop-to="moveOrCreateNewFrame"
/>
<div role="row" class="c-fl-container__frames-holder">
<div role="row" class="c-fl-container__frames-holder" :class="flexLayoutCssClass">
<template v-for="(frame, i) in frames" :key="frame.id">
<frame-component
class="c-fl-container__frame"
@ -118,6 +118,9 @@ export default {
},
emits: ['new-frame', 'move-frame', 'persist'],
computed: {
flexLayoutCssClass() {
return this.rowsLayout ? '--layout-rows' : '--layout-cols';
},
frames() {
return this.container.frames;
},

View File

@ -30,10 +30,8 @@
<div
class="c-fl__container-holder u-style-receiver js-style-receiver"
:class="{
'c-fl--rows': rowsLayout === true
}"
:aria-label="`Flexible Layout ${rowsLayout ? 'Row' : 'Column'}`"
:class="flexLayoutCssClass"
:aria-label="`Flexible Layout ${rowsLayout ? 'Rows' : 'Columns'}`"
>
<template v-for="(container, index) in containers" :key="`component-${container.id}`">
<drop-hint
@ -45,7 +43,6 @@
/>
<container-component
class="c-fl__container"
:index="index"
:container="container"
:rows-layout="rowsLayout"
@ -148,15 +145,11 @@ export default {
};
},
computed: {
layoutDirectionStr() {
if (this.rowsLayout) {
return 'Rows';
} else {
return 'Columns';
}
},
allContainersAreEmpty() {
return this.containers.every((container) => container.frames.length === 0);
},
flexLayoutCssClass() {
return this.rowsLayout ? 'c-fl--rows' : 'c-fl--cols';
}
},
created() {

View File

@ -7,7 +7,6 @@
$majorOffset: 35%;
content: '';
display: block;
position: absolute;
@include grippy($c: $editFrameSelectedMovebarColorFg, $dir: $dir);
@if $dir == 'x' {
top: $minorOffset;
@ -35,18 +34,15 @@
flex: 1 1 100%; // Must be 100% to work
overflow: auto;
// Columns by default
flex-direction: row;
> * + * {
margin-left: 1px;
// Controls layout of c-fl__container(s)
&[class*='--cols'] {
flex-direction: row;
column-gap: 1px;
}
&[class*='--rows'] {
flex-direction: column;
> * + * {
margin-left: 0;
margin-top: 1px;
}
row-gap: 1px;
}
}
@ -119,10 +115,18 @@
&__frames-holder {
display: flex;
flex: 1 1 100%; // Must be 100% to work
flex-direction: column; // Default
flex-direction: row; // Default
align-content: stretch;
align-items: stretch;
overflow: hidden; // This sucks, but doing in the short-term
&.--layout-cols {
flex-direction: column !important;
}
&.--layout-rows {
flex-direction: row !important;
}
}
.is-editing & {

View File

@ -61,13 +61,13 @@ function ToolbarProvider(openmct) {
options: [
{
value: true,
icon: 'icon-columns',
title: 'Columns layout'
icon: 'icon-rows',
title: 'Switch to rows layout'
},
{
value: false,
icon: 'icon-rows',
title: 'Rows layout'
icon: 'icon-columns',
title: 'Switch to columns layout'
}
]
};

View File

@ -26,19 +26,22 @@ import { v4 as uuid } from 'uuid';
import CreateWizard from './CreateWizard.js';
import PropertiesAction from './PropertiesAction.js';
export default class CreateAction extends PropertiesAction {
const CREATE_ACTION_KEY = 'create';
class CreateAction extends PropertiesAction {
#transaction;
constructor(openmct, type, parentDomainObject) {
constructor(openmct) {
super(openmct);
this.type = type;
this.parentDomainObject = parentDomainObject;
this.#transaction = null;
this.key = CREATE_ACTION_KEY;
// Hide the create action from context menus by default
this.isHidden = true;
}
invoke() {
this._showCreateForm(this.type);
get invoke() {
return (type, parentDomainObject) => this._showCreateForm(type, parentDomainObject);
}
/**
@ -142,7 +145,7 @@ export default class CreateAction extends PropertiesAction {
/**
* @private
*/
_showCreateForm(type) {
_showCreateForm(type, parentDomainObject) {
const typeDefinition = this.openmct.types.get(type);
const definition = typeDefinition.definition;
const domainObject = {
@ -150,7 +153,7 @@ export default class CreateAction extends PropertiesAction {
type,
identifier: {
key: uuid(),
namespace: this.parentDomainObject.identifier.namespace
namespace: parentDomainObject.identifier.namespace
}
};
@ -160,7 +163,7 @@ export default class CreateAction extends PropertiesAction {
definition.initialize(this.domainObject);
}
const createWizard = new CreateWizard(this.openmct, this.domainObject, this.parentDomainObject);
const createWizard = new CreateWizard(this.openmct, this.domainObject, parentDomainObject);
const formStructure = createWizard.getFormStructure(true);
formStructure.title = 'Create a New ' + definition.name;
@ -191,3 +194,7 @@ export default class CreateAction extends PropertiesAction {
this.#transaction = null;
}
}
export { CREATE_ACTION_KEY };
export default CreateAction;

View File

@ -22,7 +22,7 @@
import { debounce } from 'lodash';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import CreateAction from './CreateAction.js';
import { CREATE_ACTION_KEY } from './CreateAction.js';
let parentObject;
let parentObjectPath;
@ -115,8 +115,8 @@ describe('The create action plugin', () => {
const deBouncedCallback = debounce(callback, 300);
unObserve = openmct.objects.observe(parentObject, '*', deBouncedCallback);
const createAction = new CreateAction(openmct, type, parentObject);
createAction.invoke();
const createAction = openmct.actions.getAction(CREATE_ACTION_KEY);
createAction.invoke(type, parentObject);
});
});
});

View File

@ -24,13 +24,14 @@ import _ from 'lodash';
import CreateWizard from './CreateWizard.js';
import PropertiesAction from './PropertiesAction.js';
const EDIT_PROPERTIES_ACTION_KEY = 'properties';
export default class EditPropertiesAction extends PropertiesAction {
class EditPropertiesAction extends PropertiesAction {
constructor(openmct) {
super(openmct);
this.name = 'Edit Properties...';
this.key = 'properties';
this.key = EDIT_PROPERTIES_ACTION_KEY;
this.description = 'Edit properties of this object.';
this.cssClass = 'major icon-pencil';
this.hideInDefaultMenu = true;
@ -100,3 +101,7 @@ export default class EditPropertiesAction extends PropertiesAction {
.catch(this._onCancel.bind(this));
}
}
export { EDIT_PROPERTIES_ACTION_KEY };
export default EditPropertiesAction;

View File

@ -20,10 +20,12 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import CreateAction from './CreateAction.js';
import EditPropertiesAction from './EditPropertiesAction.js';
export default function () {
return function (openmct) {
openmct.actions.register(new EditPropertiesAction(openmct));
openmct.actions.register(new CreateAction(openmct));
};
}

View File

@ -164,7 +164,7 @@ describe('Gauge plugin', () => {
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
spyOn(openmct.time, 'getBounds').and.returnValue({
start: 1000,
end: 5000
});
@ -306,7 +306,7 @@ describe('Gauge plugin', () => {
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
spyOn(openmct.time, 'getBounds').and.returnValue({
start: 1000,
end: 5000
});
@ -448,7 +448,7 @@ describe('Gauge plugin', () => {
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
spyOn(openmct.time, 'getBounds').and.returnValue({
start: 1000,
end: 5000
});
@ -763,7 +763,7 @@ describe('Gauge plugin', () => {
})
});
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
spyOn(openmct.time, 'getBounds').and.returnValue({
start: 1000,
end: 5000
});

View File

@ -363,7 +363,7 @@ export default {
rangeLow: gaugeController.min,
gaugeType: gaugeController.gaugeType,
showUnits: gaugeController.showUnits,
activeTimeSystem: this.openmct.time.timeSystem(),
activeTimeSystem: this.openmct.time.getTimeSystem(),
units: ''
};
},
@ -545,7 +545,7 @@ export default {
this.composition.load();
this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('boundsChanged', this.refreshData);
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.setupClockChangedEvent((domainObject) => {
@ -561,7 +561,7 @@ export default {
this.unsubscribe();
}
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('boundsChanged', this.refreshData);
this.openmct.time.off('timeSystem', this.setTimeSystem);
},
methods: {
@ -726,7 +726,7 @@ export default {
return;
}
const { start, end } = this.openmct.time.bounds();
const { start, end } = this.openmct.time.getBounds();
const parsedValue = this.timeFormatter.parse(this.datum);
const beforeStartOfBounds = parsedValue < start;

View File

@ -84,11 +84,12 @@ $meterNeedleBorderRadius: 5px;
fill: $colorGaugeValue;
}
&__needle-value {
fill: $colorGaugeValue;
fill: $colorGaugeNeedle;
}
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
font-family: $numericFont;
}
&__units-text,
@ -125,7 +126,8 @@ $meterNeedleBorderRadius: 5px;
// Filled area
position: absolute;
background: $colorGaugeValue;
z-index: 1;
box-shadow: $gaugeMeterValueShadow 0px 2px 10px 1px;
//z-index: 3;
}
&__value-needle {
@ -135,6 +137,7 @@ $meterNeedleBorderRadius: 5px;
content: '';
display: block;
background: $colorGaugeValue;
}
}
@ -158,7 +161,7 @@ $meterNeedleBorderRadius: 5px;
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
font-family: $numericFont;
}
.c-gauge__curval {

View File

@ -20,10 +20,12 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class GoToOriginalAction {
const GO_TO_ORIGINAL_ACTION_KEY = 'goToOriginal';
class GoToOriginalAction {
constructor(openmct) {
this.name = 'Go To Original';
this.key = 'goToOriginal';
this.key = GO_TO_ORIGINAL_ACTION_KEY;
this.description = 'Go to the original unlinked instance of this object';
this.group = 'action';
this.priority = 4;
@ -62,3 +64,7 @@ export default class GoToOriginalAction {
return parentKeystring !== objectPath[0].location;
}
}
export { GO_TO_ORIGINAL_ACTION_KEY };
export default GoToOriginalAction;

View File

@ -20,14 +20,15 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class OpenImageInNewTabAction {
const OPEN_IMAGE_IN_NEW_TAB_ACTION_KEY = 'openImageInNewTab';
class OpenImageInNewTabAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-new-window';
this.description = 'Open the image in a new tab';
this.group = 'action';
this.key = 'openImageInNewTab';
this.key = OPEN_IMAGE_IN_NEW_TAB_ACTION_KEY;
this.name = 'Open Image in New Tab';
this.priority = 1;
}
@ -44,3 +45,7 @@ export default class OpenImageInNewTabAction {
}
}
}
export { OPEN_IMAGE_IN_NEW_TAB_ACTION_KEY };
export default OpenImageInNewTabAction;

View File

@ -20,14 +20,15 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class SaveImageAction {
const SAVE_IMAGE_ACTION_KEY = 'saveImageAs';
class SaveImageAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-save-as';
this.description = 'Save image to file';
this.group = 'action';
this.key = 'saveImageAs';
this.key = SAVE_IMAGE_ACTION_KEY;
this.name = 'Save Image As';
this.priority = 1;
}
@ -65,3 +66,7 @@ export default class SaveImageAction {
}
}
}
export { SAVE_IMAGE_ACTION_KEY };
export default SaveImageAction;

View File

@ -38,13 +38,15 @@ import isEqual from 'lodash/isEqual';
import { toRaw } from 'vue';
import TagEditorClassNames from '../../inspectorViews/annotations/tags/TagEditorClassNames.js';
import { OPEN_IMAGE_IN_NEW_TAB_ACTION_KEY } from '../actions/OpenImageInNewTabAction.js';
import { SAVE_IMAGE_ACTION_KEY } from '../actions/SaveImageAsAction.js';
const EXISTING_ANNOTATION_STROKE_STYLE = '#D79078';
const EXISTING_ANNOTATION_FILL_STYLE = 'rgba(202, 202, 142, 0.2)';
const SELECTED_ANNOTATION_STROKE_COLOR = '#BD8ECC';
const SELECTED_ANNOTATION_FILL_STYLE = 'rgba(199, 87, 231, 0.2)';
const CONTEXT_MENU_ACTIONS = ['openImageInNewTab', 'saveImageAs'];
const CONTEXT_MENU_ACTIONS = [OPEN_IMAGE_IN_NEW_TAB_ACTION_KEY, SAVE_IMAGE_ACTION_KEY];
export default {
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],

View File

@ -32,7 +32,7 @@ import _ from 'lodash';
import mount from 'utils/mount';
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
import PreviewAction from '@/ui/preview/PreviewAction';
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
import imageryData from '../../imagery/mixins/imageryData.js';
@ -49,7 +49,7 @@ export default {
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath'],
data() {
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
this.metadata = {};
this.requestCount = 0;
@ -71,7 +71,7 @@ export default {
}
},
mounted() {
this.previewAction = new PreviewAction(this.openmct);
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
this.canvas.height = 0;
@ -109,12 +109,12 @@ export default {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('timeSystem', this.setScaleAndPlotImagery);
this.timeContext.on('bounds', this.updateViewBounds);
this.timeContext.on('boundsChanged', this.updateViewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('timeSystem', this.setScaleAndPlotImagery);
this.timeContext.off('bounds', this.updateViewBounds);
this.timeContext.off('boundsChanged', this.updateViewBounds);
}
},
expand(imageTimestamp) {
@ -148,10 +148,10 @@ export default {
return clientWidth;
},
updateViewBounds(bounds, isTick) {
this.viewBounds = this.timeContext.bounds();
this.viewBounds = this.timeContext.getBounds();
if (this.timeSystem === undefined) {
this.timeSystem = this.timeContext.timeSystem();
this.timeSystem = this.timeContext.getTimeSystem();
}
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
@ -216,7 +216,7 @@ export default {
}
if (timeSystem === undefined) {
timeSystem = this.timeContext.timeSystem();
timeSystem = this.timeContext.getTimeSystem();
}
if (timeSystem.isUTCBased) {

View File

@ -213,8 +213,10 @@ import _ from 'lodash';
import moment from 'moment';
import { nextTick } from 'vue';
import { TIME_CONTEXT_EVENTS } from '../../../api/time/constants.js';
import imageryData from '../../imagery/mixins/imageryData.js';
import { TIME_CONTEXT_EVENTS } from '@/api/time/constants.js';
import imageryData from '@/plugins/imagery/mixins/imageryData.js';
import { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js';
import eventHelpers from '../lib/eventHelpers.js';
import AnnotationsCanvas from './AnnotationsCanvas.vue';
import Compass from './Compass/CompassComponent.vue';
@ -827,7 +829,9 @@ export default {
this.currentView
);
const visibleActions = actionCollection.getVisibleActions();
const viewLargeAction = visibleActions?.find((action) => action.key === 'large.view');
const viewLargeAction = visibleActions?.find(
(action) => action.key === VIEW_LARGE_ACTION_KEY
);
if (viewLargeAction?.appliesTo(this.objectPath, this.currentView)) {
viewLargeAction.invoke(this.objectPath, this.currentView);

View File

@ -44,7 +44,7 @@ export default class RelatedTelemetry {
this.keys = telemetryKeys;
this._timeFormatter = undefined;
this._timeSystemChange(this.timeContext.timeSystem());
this._timeSystemChange(this.timeContext.getTimeSystem());
// grab related telemetry metadata
for (let key of this.keys) {
@ -110,10 +110,10 @@ export default class RelatedTelemetry {
// and set bounds.
ephemeralContext.resetContext();
const newBounds = {
start: this.timeContext.bounds().start,
start: this.timeContext.getBounds().start,
end: this._parseTime(datum)
};
ephemeralContext.bounds(newBounds);
ephemeralContext.setBounds(newBounds);
const options = {
start: newBounds.start,

View File

@ -41,7 +41,7 @@ const helperFunctions = {
} else if (object.addEventListener) {
object.addEventListener(event, listener._cb);
} else {
object.on(event, listener._cb);
object.on(event, listener._cb, listener.context);
}
this._listeningTo.push(listener);
@ -78,7 +78,7 @@ const helperFunctions = {
} else if (listener.object.removeEventListener) {
listener.object.removeEventListener(listener.event, listener._cb);
} else {
listener.object.off(listener.event, listener._cb);
listener.object.off(listener.event, listener._cb, listener.context);
}
return listener;

View File

@ -171,7 +171,7 @@ export default {
this.bounds = bounds; // setting bounds for ImageryView watcher
},
timeSystemChanged() {
this.timeSystem = this.timeContext.timeSystem();
this.timeSystem = this.timeContext.getTimeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(

View File

@ -28,6 +28,8 @@ import {
} from 'utils/testing';
import { nextTick } from 'vue';
import { PREVIEW_ACTION_KEY } from '../../ui/preview/PreviewAction.js';
const ONE_MINUTE = 1000 * 60;
const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
@ -81,11 +83,10 @@ describe('The Imagery View Layouts', () => {
const START = Date.now();
const COUNT = 10;
// let resolveFunction;
let originalRouterPath;
let telemetryPromise;
let telemetryPromiseResolve;
let cleanupFirst;
let previewAction;
let openmct;
let parent;
@ -172,8 +173,6 @@ describe('The Imagery View Layouts', () => {
// this setups up the app
beforeEach((done) => {
cleanupFirst = [];
openmct = createOpenMct();
telemetryPromise = new Promise((resolve) => {
@ -193,6 +192,8 @@ describe('The Imagery View Layouts', () => {
return telemetryPromise;
});
previewAction = openmct.actions.getAction(PREVIEW_ACTION_KEY);
parent = document.createElement('div');
parent.style.width = '640px';
parent.style.height = '480px';
@ -222,20 +223,11 @@ describe('The Imagery View Layouts', () => {
openmct.startHeadless();
});
afterEach((done) => {
afterEach(async () => {
openmct.router.path = originalRouterPath;
document.body.removeChild(parent);
// Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after
// teardown, which causes problems
// This is hacky, we should find a better approach here.
setTimeout(() => {
//Cleanup code that needs to happen before dom elements start being destroyed
cleanupFirst.forEach((cleanup) => cleanup());
cleanupFirst = [];
document.body.removeChild(parent);
resetApplicationState(openmct).then(done).catch(done);
});
await resetApplicationState(openmct);
});
it('should provide an imagery time strip view when in a time strip', () => {
@ -609,7 +601,7 @@ describe('The Imagery View Layouts', () => {
imageryTimeView.show(child);
componentView = imageryTimeView.getComponent().$refs.root;
spyOn(componentView.previewAction, 'invoke').and.callThrough();
spyOn(previewAction, 'invoke').and.callThrough();
return nextTick();
});
@ -633,13 +625,12 @@ describe('The Imagery View Layouts', () => {
imageWrapper[2].dispatchEvent(mouseDownEvent);
await nextTick();
const timestamp = imageWrapper[2].id.replace('wrapper-', '');
const mockInvoke = componentView.previewAction.invoke;
// Make sure the function was called
expect(mockInvoke).toHaveBeenCalled();
expect(previewAction.invoke).toHaveBeenCalled();
// Get the arguments of the first call
const firstArg = mockInvoke.calls.mostRecent().args[0];
const secondArg = mockInvoke.calls.mostRecent().args[1];
const firstArg = previewAction.invoke.calls.mostRecent().args[0];
const secondArg = previewAction.invoke.calls.mostRecent().args[1];
// Compare the first argument
expect(firstArg).toEqual([componentView.objectPath[0]]);

View File

@ -24,10 +24,12 @@ import { parseKeyString } from 'objectUtils';
import { filter__proto__ } from 'utils/sanitization';
import { v4 as uuid } from 'uuid';
export default class ImportAsJSONAction {
const IMPORT_FROM_JSON_ACTION_KEY = 'import.JSON';
class ImportFromJSONAction {
constructor(openmct) {
this.name = 'Import from JSON';
this.key = 'import.JSON';
this.key = IMPORT_FROM_JSON_ACTION_KEY;
this.description = '';
this.cssClass = 'icon-import';
this.group = 'import';
@ -144,24 +146,132 @@ export default class ImportAsJSONAction {
return Array.from(new Set([...objectIdentifiers, ...itemObjectReferences]));
}
/**
* @private
* @param {Object} tree
* @param {string} namespace
* @returns {Object}
*/
_generateNewIdentifiers(tree, newNamespace) {
// For each domain object in the file, generate new ID, replace in tree
Object.keys(tree.openmct).forEach((domainObjectId) => {
const oldId = parseKeyString(domainObjectId);
/**
* Generates a map of old IDs to new IDs for efficient lookup during tree walking.
* This function considers cases where original namespaces are blank and updates those IDs as well.
*
* @param {Object} tree - The object tree containing the old IDs.
* @param {string} newNamespace - The namespace for the new IDs.
* @returns {Object} A map of old IDs to new IDs.
*/
_generateIdMap(tree, newNamespace) {
const idMap = {};
const keys = Object.keys(tree.openmct);
for (const oldIdKey of keys) {
const oldId = parseKeyString(oldIdKey);
const newId = {
namespace: newNamespace,
key: uuid()
};
tree = this._rewriteId(oldId, newId, tree);
}, this);
const newIdKeyString = this.openmct.objects.makeKeyString(newId);
// Update the map with the old and new ID key strings.
idMap[oldIdKey] = newIdKeyString;
// If the old namespace is blank, also map the non-namespaced ID.
if (!oldId.namespace) {
const nonNamespacedOldIdKey = oldId.key;
idMap[nonNamespacedOldIdKey] = newIdKeyString;
}
}
return idMap;
}
/**
* Walks through the object tree and updates IDs according to the provided ID map.
* @param {Object} obj - The current object being visited in the tree.
* @param {Object} idMap - A map of old IDs to new IDs for rewriting.
* @param {Object} importDialog - Optional progress dialog for import.
* @returns {Promise<Object>} The object with updated IDs.
*/
async _walkAndRewriteIds(obj, idMap, importDialog) {
// How many rewrites to do before yielding to the event loop
const UI_UPDATE_INTERVAL = 300;
// The percentage of the progress dialog to allocate to rewriting IDs
const PERCENT_OF_DIALOG = 80;
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj === 'string') {
const possibleId = idMap[obj];
if (possibleId) {
return possibleId;
} else {
return obj;
}
}
if (Object.hasOwn(obj, 'key') && Object.hasOwn(obj, 'namespace')) {
const oldId = this.openmct.objects.makeKeyString(obj);
const possibleId = idMap[oldId];
if (possibleId) {
const newIdParts = possibleId.split(':');
if (newIdParts.length >= 2) {
// new ID is namespaced, so update both the namespace and key
obj.namespace = newIdParts[0];
obj.key = newIdParts[1];
} else {
// old ID was not namespaced, so update the key only
obj.namespace = '';
obj.key = newIdParts[0];
}
}
return obj;
}
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
obj[i] = await this._walkAndRewriteIds(obj[i], idMap); // Process each item in the array
}
return obj;
}
if (typeof obj === 'object') {
const newObj = {};
const keys = Object.keys(obj);
let processedCount = 0;
for (const key of keys) {
const value = obj[key];
const possibleId = idMap[key];
const newKey = possibleId || key;
newObj[newKey] = await this._walkAndRewriteIds(value, idMap);
// Optionally update the importDialog here, after each property has been processed
if (importDialog) {
processedCount++;
if (processedCount % UI_UPDATE_INTERVAL === 0) {
// yield to the event loop to allow the UI to update
await new Promise((resolve) => setTimeout(resolve, 0));
const percentPersisted = Math.ceil(PERCENT_OF_DIALOG * (processedCount / keys.length));
const message = `Rewriting ${processedCount} of ${keys.length} imported objects.`;
importDialog.updateProgress(percentPersisted, message);
}
}
}
return newObj;
}
// Return the input as-is for types that are not objects, strings, or arrays
return obj;
}
/**
* @private
* @param {Object} tree
* @returns {Promise<Object>}
*/
async _generateNewIdentifiers(tree, newNamespace, importDialog) {
const idMap = this._generateIdMap(tree, newNamespace);
tree.rootId = idMap[tree.rootId];
tree.openmct = await this._walkAndRewriteIds(tree.openmct, idMap, importDialog);
return tree;
}
/**
@ -170,9 +280,16 @@ export default class ImportAsJSONAction {
* @param {Object} objTree
*/
async _importObjectTree(domainObject, objTree) {
// make rewriting objects IDs 80% of the progress bar
const importDialog = this.openmct.overlays.progressDialog({
progressPerc: 0,
message: `Importing ${Object.keys(objTree.openmct).length} objects`,
iconClass: 'info',
title: 'Importing'
});
const objectsToCreate = [];
const namespace = domainObject.identifier.namespace;
const tree = this._generateNewIdentifiers(objTree, namespace);
const tree = await this._generateNewIdentifiers(objTree, namespace, importDialog);
const rootId = tree.rootId;
const rootObj = tree.openmct[rootId];
@ -182,11 +299,24 @@ export default class ImportAsJSONAction {
this._deepInstantiate(rootObj, tree.openmct, [], objectsToCreate);
try {
await Promise.all(objectsToCreate.map(this._instantiate, this));
let persistedObjects = 0;
// make saving objects objects 20% of the progress bar
await Promise.all(
objectsToCreate.map(async (objectToCreate) => {
persistedObjects++;
const percentPersisted =
Math.ceil(20 * (persistedObjects / objectsToCreate.length)) + 80;
const message = `Saving ${persistedObjects} of ${objectsToCreate.length} imported objects.`;
importDialog.updateProgress(percentPersisted, message);
await this._instantiate(objectToCreate);
})
);
} catch (error) {
this.openmct.notifications.error('Error saving objects');
throw error;
} finally {
importDialog.dismiss();
}
const compositionCollection = this.openmct.composition.get(domainObject);
@ -194,7 +324,8 @@ export default class ImportAsJSONAction {
this.openmct.objects.mutate(rootObj, 'location', domainObjectKeyString);
compositionCollection.add(rootObj);
} else {
const dialog = this.openmct.overlays.dialog({
importDialog.dismiss();
const cannotImportDialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: "We're sorry, but you cannot import that object type into this object.",
buttons: [
@ -202,7 +333,7 @@ export default class ImportAsJSONAction {
label: 'Ok',
emphasis: true,
callback: function () {
dialog.dismiss();
cannotImportDialog.dismiss();
}
}
]
@ -217,43 +348,7 @@ export default class ImportAsJSONAction {
_instantiate(model) {
return this.openmct.objects.save(model);
}
/**
* @private
* @param {Object} oldId
* @param {Object} newId
* @param {Object} tree
* @returns {Object}
*/
_rewriteId(oldId, newId, tree) {
let newIdKeyString = this.openmct.objects.makeKeyString(newId);
let oldIdKeyString = this.openmct.objects.makeKeyString(oldId);
const newTreeString = JSON.stringify(tree).replace(
new RegExp(oldIdKeyString, 'g'),
newIdKeyString
);
const newTree = JSON.parse(newTreeString, (key, value) => {
if (
value !== undefined &&
value !== null &&
Object.prototype.hasOwnProperty.call(value, 'key') &&
Object.prototype.hasOwnProperty.call(value, 'namespace')
) {
// first check if key is messed up from regex and contains a colon
// if it does, repair it
if (value.key.includes(':')) {
const splitKey = value.key.split(':');
value.key = splitKey[1];
value.namespace = splitKey[0];
}
// now check if we need to replace the id
if (value.key === oldId.key && value.namespace === oldId.namespace) {
return newId;
}
}
return value;
});
return newTree;
}
/**
* @private
* @param {Object} domainObject
@ -312,3 +407,7 @@ export default class ImportAsJSONAction {
return success;
}
}
export { IMPORT_FROM_JSON_ACTION_KEY };
export default ImportFromJSONAction;

View File

@ -111,7 +111,6 @@ describe('The import JSON action', function () {
});
it('protects against prototype pollution', (done) => {
spyOn(console, 'warn');
spyOn(openmct.forms, 'showForm').and.callFake(returnResponseWithPrototypePollution);
unObserve = openmct.objects.observe(folderObject, '*', callback);
@ -123,8 +122,6 @@ describe('The import JSON action', function () {
Object.prototype.hasOwnProperty.call(newObject, '__proto__') ||
Object.prototype.hasOwnProperty.call(Object.getPrototypeOf(newObject), 'toString');
// warning from openmct.objects.get
expect(console.warn).not.toHaveBeenCalled();
expect(hasPollutedProto).toBeFalse();
done();
@ -192,6 +189,12 @@ describe('The import JSON action', function () {
type: 'folder'
};
spyOn(openmct.objects, 'save').and.callFake((model) => Promise.resolve(model));
spyOn(openmct.overlays, 'progressDialog').and.callFake(() => {
return {
updateProgress: () => {},
dismiss: () => {}
};
});
try {
await importFromJSONAction.onSave(targetDomainObject, {
selectFile: { body: JSON.stringify(incomingObject) }

View File

@ -66,6 +66,9 @@
</template>
<script>
import { NEW_TAB_ACTION_KEY } from '@/plugins/openInNewTabAction/openInNewTabAction.js';
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
export default {
inject: ['openmct'],
provide() {
@ -119,7 +122,7 @@ export default {
'tc.endBound': timeBounds?.end,
'tc.mode': 'fixed'
};
const newTabAction = this.openmct.actions.getAction('newTab');
const newTabAction = this.openmct.actions.getAction(NEW_TAB_ACTION_KEY);
// No view context needed, so pass undefined.
// The urlParams arg will override the global time bounds with the data visualization
// plot bounds.
@ -127,7 +130,7 @@ export default {
this.showMenu = false;
},
previewTelemetry() {
const previewAction = this.openmct.actions.getAction('preview');
const previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
previewAction.invoke([this.telemetryObject]);
this.showMenu = false;
}

View File

@ -181,3 +181,7 @@
}
}
}
.c-tag-applier__add-btn.c-icon-button.c-icon-button--major.icon-plus {
color: $colorKey !important;
}

View File

@ -20,10 +20,12 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class LinkAction {
const LINK_ACTION_KEY = 'link';
class LinkAction {
constructor(openmct) {
this.name = 'Create Link';
this.key = 'link';
this.key = LINK_ACTION_KEY;
this.description = 'Create Link to object in another location.';
this.cssClass = 'icon-link';
this.group = 'action';
@ -154,3 +156,7 @@ export default class LinkAction {
this.transaction = null;
}
}
export { LINK_ACTION_KEY };
export default LinkAction;

View File

@ -21,7 +21,7 @@
*****************************************************************************/
import { createOpenMct, getMockObjects, resetApplicationState } from 'utils/testing';
import LinkAction from './LinkAction.js';
import { LINK_ACTION_KEY } from './LinkAction.js';
import LinkActionPlugin from './plugin.js';
describe('The Link Action plugin', () => {
@ -31,8 +31,7 @@ describe('The Link Action plugin', () => {
let parentObject;
let anotherParentObject;
const ORIGINAL_PARENT_ID = 'original-parent-object';
const LINK_ACITON_KEY = 'link';
const LINK_ACITON_NAME = 'Create Link';
const LINK_ACTION_NAME = 'Create Link';
beforeEach((done) => {
const appHolder = document.createElement('div');
@ -97,14 +96,14 @@ describe('The Link Action plugin', () => {
it('should make the link action available for an appropriate domainObject', () => {
const actionCollection = openmct.actions.getActionsCollection([childObject]);
const visibleActions = actionCollection.getVisibleActions();
linkAction = visibleActions.find((a) => a.key === LINK_ACITON_KEY);
linkAction = visibleActions.find((a) => a.key === LINK_ACTION_KEY);
expect(linkAction.name).toEqual(LINK_ACITON_NAME);
expect(linkAction.name).toEqual(LINK_ACTION_NAME);
});
describe('when linking an object in a new parent', () => {
beforeEach(() => {
linkAction = new LinkAction(openmct);
linkAction = openmct.actions.getAction(LINK_ACTION_KEY);
linkAction.linkInNewParent(childObject, anotherParentObject);
});

View File

@ -19,10 +19,13 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class MoveAction {
const MOVE_ACTION_KEY = 'move';
class MoveAction {
constructor(openmct) {
this.name = 'Move';
this.key = 'move';
this.key = MOVE_ACTION_KEY;
this.description = 'Move this object from its containing object to another object.';
this.cssClass = 'icon-move';
this.group = 'action';
@ -216,3 +219,7 @@ export default class MoveAction {
this.transaction = null;
}
}
export { MOVE_ACTION_KEY };
export default MoveAction;

View File

@ -19,13 +19,14 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import CreateAction from '@/plugins/formActions/CreateAction';
export default class NewFolderAction {
const NEW_FOLDER_ACTION_KEY = 'newFolder';
class NewFolderAction {
constructor(openmct) {
this.type = 'folder';
this.name = 'Add New Folder';
this.key = 'newFolder';
this.key = NEW_FOLDER_ACTION_KEY;
this.description = 'Create a new folder';
this.cssClass = 'icon-folder-new';
this.group = 'action';
@ -36,8 +37,8 @@ export default class NewFolderAction {
invoke(objectPath) {
const parentDomainObject = objectPath[0];
const createAction = new CreateAction(this._openmct, this.type, parentDomainObject);
createAction.invoke();
const createAction = this._openmct.actions.getAction('create');
createAction.invoke(this.type, parentDomainObject);
}
appliesTo(objectPath) {
@ -47,3 +48,7 @@ export default class NewFolderAction {
return domainObject.type === this.type && isPersistable;
}
}
export { NEW_FOLDER_ACTION_KEY };
export default NewFolderAction;

View File

@ -1,14 +1,15 @@
import { addNotebookEntry } from '../utils/notebook-entries.js';
import { getDefaultNotebook, getNotebookSectionAndPage } from '../utils/notebook-storage.js';
export default class CopyToNotebookAction {
const COPY_TO_NOTEBOOK_ACTION_KEY = 'copyToNotebook';
class CopyToNotebookAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-duplicate';
this.description = 'Copy value to notebook as an entry';
this.group = 'action';
this.key = 'copyToNotebook';
this.key = COPY_TO_NOTEBOOK_ACTION_KEY;
this.name = 'Copy to Notebook';
this.priority = 1;
}
@ -49,3 +50,7 @@ export default class CopyToNotebookAction {
return row.formattedValueForCopy && typeof row.formattedValueForCopy === 'function';
}
}
export { COPY_TO_NOTEBOOK_ACTION_KEY };
export default CopyToNotebookAction;

View File

@ -5,15 +5,16 @@ import { NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants.j
const UNKNOWN_USER = 'Unknown';
const UNKNOWN_TIME = 'Unknown';
const ALLOWED_TYPES = [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE];
const EXPORT_NOTEBOOK_AS_TEXT_ACTION_KEY = 'exportNotebookAsText';
export default class ExportNotebookAsTextAction {
class ExportNotebookAsTextAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-export';
this.description = 'Exports notebook contents as a text file';
this.group = 'export';
this.key = 'exportNotebookAsText';
this.key = EXPORT_NOTEBOOK_AS_TEXT_ACTION_KEY;
this.name = 'Export Notebook as Text';
}
@ -179,3 +180,7 @@ export default class ExportNotebookAsTextAction {
return this.onSave(changes, objectPath);
}
}
export { EXPORT_NOTEBOOK_AS_TEXT_ACTION_KEY };
export default ExportNotebookAsTextAction;

View File

@ -680,7 +680,7 @@ export default {
} else if (domainObjectData) {
// plain domain object
const objectPath = JSON.parse(domainObjectData);
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const snapshotMeta = {
bounds,
link: null,

View File

@ -54,10 +54,10 @@ import Moment from 'moment';
import mount from 'utils/mount';
import { objectPathToUrl } from '@/tools/url';
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
import ImageExporter from '../../../exporters/ImageExporter.js';
import PreviewAction from '../../../ui/preview/PreviewAction.js';
import { updateNotebookImageDomainObject } from '../utils/notebook-image.js';
import PainterroInstance from '../utils/painterroInstance.js';
import RemoveDialog from '../utils/removeDialog.js';
@ -275,10 +275,10 @@ export default {
}
const hash = this.embed.historicLink;
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const isTimeBoundChanged =
this.embed.bounds.start !== bounds.start || this.embed.bounds.end !== bounds.end;
const isFixedTimespanMode = !this.openmct.time.clock();
const isFixedTimespanMode = this.openmct.time.isFixed();
let message = '';
if (isTimeBoundChanged) {
@ -393,10 +393,9 @@ export default {
}
},
previewEmbed() {
const self = this;
const previewAction = new PreviewAction(self.openmct);
const previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
this.openmct.objects
.get(self.embed.domainObject.identifier)
.get(this.embed.domainObject.identifier)
.then((domainObject) => previewAction.invoke([domainObject]));
},
removeEmbed(success) {

View File

@ -31,7 +31,7 @@
@drop.capture="cancelEditMode"
@drop.prevent="dropOnEntry"
@click="selectAndEmitEntry($event, entry)"
@paste="addImageFromPaste"
@paste="handlePaste"
>
<div class="c-ne__time-and-content">
<div class="c-ne__time-and-creator-and-delete">
@ -368,8 +368,30 @@ export default {
}
},
methods: {
handlePaste(event) {
const clipboardItems = Array.from(
(event.clipboardData || event.originalEvent.clipboardData).items
);
const hasClipboardText = clipboardItems.some(
(clipboardItem) => clipboardItem.kind === 'string'
);
const clipboardImages = clipboardItems.filter(
(clipboardItem) => clipboardItem.kind === 'file' && clipboardItem.type.includes('image')
);
const hasClipboardImages = clipboardImages?.length > 0;
if (hasClipboardImages) {
if (hasClipboardText) {
console.warn('Image and text kinds found in paste. Only processing images.');
}
this.addImageFromPaste(clipboardImages, event);
} else if (hasClipboardText) {
this.addTextFromPaste(event);
}
},
async addNewEmbed(objectPath) {
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const snapshotMeta = {
bounds,
link: null,
@ -384,32 +406,34 @@ export default {
this.manageEmbedLayout();
},
async addImageFromPaste(event) {
const clipboardItems = Array.from(
(event.clipboardData || event.originalEvent.clipboardData).items
);
const hasImage = clipboardItems.some(
(clipboardItem) => clipboardItem.type.includes('image') && clipboardItem.kind === 'file'
);
// If the clipboard contained an image, prevent the paste event from reaching the textarea.
if (hasImage) {
addTextFromPaste(event) {
if (!this.editMode) {
event.preventDefault();
}
},
async addImageFromPaste(clipboardImages, event) {
event?.preventDefault();
let updated = false;
await Promise.all(
Array.from(clipboardItems).map(async (clipboardItem) => {
const isImage = clipboardItem.type.includes('image') && clipboardItem.kind === 'file';
if (isImage) {
const imageFile = clipboardItem.getAsFile();
const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name);
if (!this.entry.embeds) {
this.entry.embeds = [];
}
this.entry.embeds.push(imageEmbed);
Array.from(clipboardImages).map(async (clipboardImage) => {
const imageFile = clipboardImage.getAsFile();
const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name);
if (!this.entry.embeds) {
this.entry.embeds = [];
}
this.entry.embeds.push(imageEmbed);
updated = true;
})
);
this.manageEmbedLayout();
this.timestampAndUpdate();
if (updated) {
this.manageEmbedLayout();
this.timestampAndUpdate();
}
},
convertMarkDownToHtml(text = '') {
let markDownHtml = this.marked.parse(text, {

View File

@ -123,7 +123,7 @@ export default {
const objectPath = this.objectPath || this.openmct.router.path;
const link = this.isPreview ? this.getPreviewObjectLink() : window.location.hash;
const snapshotMeta = {
bounds: this.openmct.time.bounds(),
bounds: this.openmct.time.getBounds(),
link,
objectPath,
openmct: this.openmct

View File

@ -140,7 +140,7 @@ export function createNewImageEmbed(image, openmct, imageName = '') {
};
const embedMetaData = {
bounds: openmct.time.bounds(),
bounds: openmct.time.getBounds(),
link: null,
objectPath: null,
openmct,

Some files were not shown because too many files have changed in this diff Show More