Compare commits

...

12 Commits

Author SHA1 Message Date
Jamie V
6633c0557a initial enabling sourcemaps 2024-02-29 13:16:12 -08:00
Charles Hacskaylo
e449fd0eda Fix visible toolbar overflow (#7037)
* Closes #7036
- CSS fix to prevent toolbar overrun of main view area.

* a11y: add label to pane handles

* test(visual): toolbar overflow

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-02-29 11:26:51 -08:00
Shefali Joshi
7d25c967a5 Allow test data toggle to be clicked (#7479)
* Add the disabled class instead of the disabled property

* Add test for condition set test data

* Use computed property for css class

* Use array syntax for class instead of computed value

* Fix .getByTitle locators to use .getByLabel instead

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-02-28 19:44:37 +00:00
Shefali Joshi
dc9bd8bcb1 Remove all logic around auto centering and scrolling the timelist (#7474) 2024-02-22 17:56:54 +00:00
Jesse Mazzella
29d83e9c6d fix(#7456): allow children of Overlay Plots to be removed (#7516)
* fix(#7456): check if object is an identifier

* refactor: use v-show, remove unnecessary deep copy, set yAxes array to empty in a way that preserves reactivity

* refactor: use ESM exports

* refactor: use ESM imports

* test(e2e): add test for element item removal from overlay plot

* a11y: add accessible name for object labels

* refactor: move overlayPlot creation to beforeAll, use getByLabel
2024-02-21 16:07:48 -08:00
Rukmini Bose (Ruki)
6393a77c19 [Mobile] Add Global Search to Mobile (#7477)
* Add status area back to mobile

* Make search results responsive to width

* Make clear search button always visible, regardless of hover

* Make clear search button visible, and fix weird margin in top left corner

* Fix input width, add logic to make close button work properly, fix margin on results so there is room for close button, fix various landscape mode issues

* update mobile testing

* Fix dropdown sizes, remove shadows and corners to make it look less like a popup and more full screen

* Add animation and persist search bar in mobile

* Fix linting issues

* Fix padding in Desktop

* fix padding in status area

* fix bad merge

* lint fixes

* fix bad merge... again

* again

* fixes to mobile

* update tests

* lint fix

* test fixes

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-02-21 15:29:38 -08:00
dependabot[bot]
6bbabf9c45 chore(deps-dev): bump vue from 3.3.8 to 3.4.19 (#7511)
Bumps [vue](https://github.com/vuejs/core) from 3.3.8 to 3.4.19.
- [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.3.8...v3.4.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-02-21 15:13:02 -08:00
Rukmini Bose (Ruki)
f18d1d2a51 Add Collapse to Recent Objects (#7502)
* initial functional implementation

* removing vestigial code

* Fix styles for button bar

* Remove bound for :persist-position

* Revert "Remove bound for :persist-position"

This reverts commit ce2ea422d7.

* test(e2e): modify existing test to verify collapse/expand recent objects pane

---------

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-02-21 21:28:29 +00:00
Jesse Mazzella
5894c66df1 docs(release.yml): make plural (#7517) 2024-02-20 14:58:44 -08:00
John Hill
6ff8c42041 docs: Update release.yml and docs (#7514)
Attempt one
2024-02-20 14:29:00 -08:00
Scott Bell
9870a6bc9c Check role when receiving status updates in the Operator Status Indicator (#7509)
* check role when receiving status updates

* pass role to get status
2024-02-16 21:45:04 +00:00
Robert Serrano Kobylyansky
317ea8c275 docs: Add openmct type hints (#7247)
* Add setAssetPath method to MCT type information

* Add TypeRegistry to OpenMCT typedef
2024-02-16 16:56:13 +00:00
57 changed files with 768 additions and 353 deletions

View File

@@ -160,6 +160,31 @@ jobs:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-mobile:
executor: pw-focal-development
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npm run test:e2e:mobile
- when:
condition:
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_e2e_code_cov_report:
suite: full
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- store_artifacts:
path: coverage
- store_artifacts:
path: html-test-results
- when:
condition:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-couchdb:
executor: ubuntu
steps:
@@ -260,8 +285,9 @@ workflows:
- e2e-test:
name: e2e-stable
suite: stable
- e2e-mobile
- visual-a11y-tests:
name: visual-test-ci
name: visual-a11y-test-ci
suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night
@@ -277,10 +303,11 @@ workflows:
- e2e-test:
name: e2e-full-nightly
suite: full
- mem-test
- e2e-mobile
- perf-test
- mem-test
- visual-a11y-tests:
name: visual-test-nightly
name: visual-a11y-test-nightly
suite: full
- e2e-couchdb
triggers:

View File

@@ -8,7 +8,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
* [ ] Is this change backwards compatible? For example, developers won't need to change how they are calling the API or how they've extended core plugins such as Tables or Plots.
* [ ] Is this a notable change that will require a special callout in the release notes [Notable Change](../docs/src/process/release.md) ? For example, will this break compatibility with existing APIs or projects which source these plugins?
### Author Checklist

5
.github/release.yml vendored
View File

@@ -1,5 +1,8 @@
changelog:
categories:
- title: 💥 Notable Changes
labels:
- notable_change
- title: 🏕 Features
labels:
- type:feature
@@ -20,4 +23,4 @@ changelog:
- dependencies
- title: 🐛 Bug Fixes
labels:
- '*'
- "*"

View File

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

View File

@@ -0,0 +1,30 @@
# Release of NASA Open MCT NPM Package
This document outlines the process and key considerations for releasing a new version of the NASA Open MCT project as an NPM (Node Package Manager) package.
## 1. Pre-requisites
Before releasing a new version of the NASA Open MCT NPM package, ensure all dependencies are updated, and comprehensive tests are performed. This ensures compatibility and performance of the Open MCT within the Node.js ecosystem.
## 2. Versioning
Versioning is a critical step for package release. The Open MCT team follows [Semantic Versioning (SemVer)](https://semver.org) that consists of three major components: MAJOR.MINOR.PATCH. These ensure a structured process for updating, bug fixes, backward compatibility, and software progress.
## 3. Changelog Maintenance
A comprehensive changelog file, `CHANGELOG.md`, documents any changes, adding a high level of transparencies for anyone desiring to look into the status of new and past progress. It includes the summation of any major new enhancements, changes, bug fixes, and the credits to the users responsible for each unique progress.
## 4. Notable Changes Labels on GitHub PRs
For the Open MCT package, we leverage GitHub's Pull Request (PR) mechanisms extensively, with three important PR labels dedicated to signifying 'notable_changes':
- **Breaking Change** Highlights the integration of changes that are suspected to break, or without a doubt will break, backward compatibility. These should signal to users the upgrade might be seamless only if dependency and integration factors are properly managed, if not, one should expect to manage atypical technical snags.
- **API change** Signifies when a contribution makes any complete or under layer changes to the communication or its supporting access processes. This label flags required see-through insight on how the web-based control panel sees and manipulates any value and or network logs.
- **Default Behavior Change:** In the incident an update either adjusts a form to or integrates a not previously kept setting or plugin. i.e. autoscale is enabled by default when working with plots.
## 6. Community & Contributions
A flat community and the rounded center are kept in continuous celebration, with the given station open for two open-specifying dialogues, research, and all-for development probing. State the ownership for a handed looped, a welcome for even structure-core and architectural draft and impend.
Thank you for your collaboration and commitment to moving the project onto a text big club.

View File

@@ -229,7 +229,7 @@ Current list of test tags:
|Test Tag|Description|
|:-:|-|
|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@mobile` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.|
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
@@ -329,9 +329,15 @@ In terms of operating system testing, we're only limited by what the CI provider
#### **Mobile**
We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
We have a Mission-need to support iPad and mobile devices. To run our test suites with mobile devices, please see our `playwright-mobile.config.js` projects.
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button) and so this will likely turn into a separate suite.
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button). To bypass the object creation, we leverage the `storageState` properties for starting the mobile tests with localstorage.
For now, the mobile tests will exist in the /tests/mobile/ suites and be executed with the
```sh
npm run test:e2e:mobile
```
command.
#### **Skipping or executing tests based on browser, os, and/os browser version:**

View File

@@ -10,6 +10,7 @@ const NUM_WORKERS = 2;
const config = {
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
grepInvert: /@mobile/, //Ignore mobile tests
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
webServer: {

View File

@@ -1,13 +1,11 @@
// playwright.config.js
// @ts-check
// eslint-disable-next-line no-unused-vars
import { devices } from '@playwright/test';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
testDir: 'tests',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
@@ -35,7 +33,6 @@ const config = {
},
{
name: 'MMOC',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@@ -47,8 +44,6 @@ const config = {
},
{
name: 'safari',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340
grepInvert: /@snapshot/,
use: {
browserName: 'webkit'
@@ -56,7 +51,6 @@ const config = {
},
{
name: 'firefox',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'firefox'
@@ -64,7 +58,6 @@ const config = {
},
{
name: 'canary',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@@ -73,22 +66,11 @@ const config = {
},
{
name: 'chrome-beta',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
channel: 'chrome-beta'
}
},
{
name: 'ipad',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/,
grepInvert: /@snapshot/,
use: {
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [

View File

@@ -0,0 +1,69 @@
// playwright.config.js
// @ts-check
import { devices } from '@playwright/test';
const MAX_FAILURES = 5;
const NUM_WORKERS = 2;
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 30 * 1000,
webServer: {
command: 'npm run start:coverage',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
},
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
use: {
baseURL: 'http://localhost:8080/',
headless: true,
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'off'
},
projects: [
{
name: 'ipad',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
},
{
name: 'iphone',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [
['list'],
[
'html',
{
open: 'never',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
}
],
['junit', { outputFile: '../test-results/results.xml' }]
]
};
export default config;

View File

@@ -1,6 +1,9 @@
// playwright.config.js
// @ts-check
import { devices } from '@playwright/test';
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0, //Retries are not needed with watch mode
@@ -28,6 +31,28 @@ const config = {
use: {
browserName: 'chromium'
}
},
{
name: 'ipad',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
},
{
name: 'iphone',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [

View File

@@ -298,7 +298,7 @@ test.describe('Basic Condition Set Use', () => {
}) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
@@ -378,4 +378,83 @@ test.describe('Basic Condition Set Use', () => {
await page.goto(conditionSet.url);
await expect(outputValue).toHaveText('---');
});
test('ConditionSet has correct outputs when test data is enabled', async ({ page }) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
await page.getByLabel('Show selected item in tree').click();
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
// Create two conditions
await page.locator('#addCondition').click();
await page.locator('#addCondition').click();
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
// Add Telemetry to ConditionSet
const sineWaveGeneratorTreeItem = page
.getByRole('tree', {
name: 'Main Tree'
})
.getByRole('treeitem', {
name: exampleTelemetry.name
});
const conditionCollection = page.locator('#conditionCollection');
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
// Modify First Criterion
const firstCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
);
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const firstCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=0'
);
firstCriterionMetadata.selectOption({ label: 'Sine' });
const firstCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=0'
);
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
await firstCriterionInput.fill('0');
// Modify Second Criterion
const secondCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
);
await secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const secondCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=1'
);
await secondCriterionMetadata.selectOption({ label: 'Sine' });
const secondCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=1'
);
await secondCriterionComparison.selectOption({ label: 'is less than' });
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
await secondCriterionInput.fill('0');
// Enable test data
await page.getByLabel('Apply Test Data').nth(1).click();
const testDataTelemetry = page.locator('[aria-label="Test Data Telemetry Selection"] >> nth=0');
await testDataTelemetry.selectOption({ label: exampleTelemetry.name });
const testDataMetadata = page.locator('[aria-label="Test Data Metadata Selection"] >> nth=0');
await testDataMetadata.selectOption({ label: 'Sine' });
const testInput = page.locator('[aria-label="Test Data Input"] >> nth=0');
await testInput.fill('0');
// 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"]');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
});
});

View File

@@ -363,7 +363,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.locator('[aria-label="Close"]').click();
await page.getByRole('button', { name: 'Close' }).click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
});
@@ -386,7 +386,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.locator('[aria-label="Close"]').click();
await page.getByRole('button', { name: 'Close' }).click();
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
});
@@ -509,7 +509,7 @@ test.describe('Example Imagery in Flexible layout', () => {
await page.getByRole('button', { name: 'Background Image', state: 'visible' });
// Close the large view
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
});
test.beforeEach(async ({ page }) => {

View File

@@ -308,7 +308,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
await page
.getByRole('treeitem', { name: overlayPlot.name })
@@ -332,7 +332,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, 'Entry to drop into');
await page
@@ -377,7 +377,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
@@ -404,7 +404,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
@@ -421,7 +421,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
@@ -438,7 +438,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`);
@@ -455,7 +455,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
@@ -483,7 +483,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
await nbUtils.enterTextEntry(
page,

View File

@@ -68,7 +68,7 @@ test.describe('Snapshot image tests', () => {
// expect large image to be displayed
await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
// drop another image onto the entry
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });

View File

@@ -33,15 +33,15 @@ import {
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Overlay Plot', () => {
let overlayPlot;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
});
test('Plot legend color is in sync with plot series color', async ({ page }) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@@ -68,9 +68,6 @@ test.describe('Overlay Plot', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7403'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
@@ -130,10 +127,6 @@ test.describe('Overlay Plot', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6338'
});
// Create an Overlay Plot with a default SWG
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
@@ -199,10 +192,6 @@ test.describe('Overlay Plot', () => {
test('The elements pool supports dragging series into multiple y-axis buckets', async ({
page
}) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@@ -292,10 +281,6 @@ test.describe('Overlay Plot', () => {
description: 'https://github.com/nasa/openmct/issues/7421'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@@ -309,12 +294,32 @@ test.describe('Overlay Plot', () => {
await page.getByRole('tab', { name: 'Elements' }).click();
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
}
);
test('Can remove an item via the elements pool action menu', async ({ page }) => {
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await waitForPlotsToRender(page);
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Elements' }).click();
const swgAElementsPoolItem = page.getByLabel(`Preview ${swgA.name}`);
await expect(swgAElementsPoolItem).toBeVisible();
await swgAElementsPoolItem.click({ button: 'right' });
await page.getByRole('menuitem', { name: 'Remove' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(swgAElementsPoolItem).toBeHidden();
});
});
/**

View File

@@ -57,7 +57,7 @@ test.describe('Plots work in Previews', () => {
await page.getByLabel('Sine', { exact: true }).click({ button: 'right' });
await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
await page.getByLabel('Expand Test Display Layout layout').click();
// change to a plot and ensure embiggen works
@@ -73,7 +73,7 @@ test.describe('Plots work in Previews', () => {
await expect(page.getByLabel('Preview Container')).toBeHidden();
await page.getByLabel('Large View').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
// get last sinewave tree item (in the display layout)
await page
@@ -83,6 +83,6 @@ test.describe('Plots work in Previews', () => {
.click({ button: 'right' });
await page.getByLabel('View', { exact: true }).click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
});
});

View File

@@ -53,7 +53,7 @@ test.describe('Preview mode', () => {
await expect(page.getByLabel('Export Table Data')).toBeVisible();
await expect(page.getByLabel('Export Marked Rows')).toBeVisible();
await page.getByRole('menuitem', { name: 'Pause' }).click();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
await expandEntireTree(page);

View File

@@ -191,7 +191,7 @@ test.describe('Recent Objects', () => {
// Navigate to the clock and reveal it in the tree
await page.goto(clock.url);
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
// Right click the clock and create an alias using the "link" context menu action
const clockTreeItem = page
@@ -298,7 +298,7 @@ test.describe('Recent Objects', () => {
// Assert that the list is empty
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0);
});
test('Ensure clear recent objects button is active or inactive', async ({ page }) => {
test('Verify functionality of "clear" and "collapse pane" buttons', async ({ page }) => {
// Assert that the list initially contains 3 objects (clock, folder, my items)
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3);
@@ -331,6 +331,24 @@ test.describe('Recent Objects', () => {
expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe(
true
);
// Assert initial state of pane and collapse the Recent Objects panel
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();
await page.getByLabel('Collapse Recently Viewed Pane').click();
// Assert that the "Expand Recently Viewed Pane" button is visible
// and that the "Collapse Recently Viewed Pane" button is hidden
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeVisible();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeHidden();
// Expand the Recent Objects panel by clicking on the "Expand Recently Viewed Pane" button
await page.getByLabel('Expand Recently Viewed Pane').click();
// Assert that the "Expand Recently Viewed Pane" button is hidden
// and that the "Collapse Recently Viewed Pane" button is visible
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();
});
function assertInitialRecentObjectsListState() {

View File

@@ -48,7 +48,7 @@ test('Verify that the create button appears and that the Folder Domain Object is
await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
});
test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => {
test('Verify that My Items Tree appears', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
//Go to baseURL
await page.goto('./');

View File

@@ -40,7 +40,7 @@ test.describe('Main Tree', () => {
type: 'Folder'
});
await page.getByTitle('Show selected item in tree').click();
await page.getByLabel('Show selected item in tree').click();
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock',

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.
*****************************************************************************/
/*
This test suite is dedicated to tests which can quickly verify that any openmct installation is
operable and that any type of testing can proceed.
Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them
more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly
as they cover a very "thin surface" of functionality.
When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel
comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects.
Make no assumptions about the order that elements appear in the DOM.
*/
import { expect, test } from '../../pluginFixtures.js';
test('Verify that My Items Tree appears @mobile', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
//Go to baseURL
await page.goto('./');
//My Items to be visible
await expect(page.getByRole('treeitem', { name: `${myItemsFolderName}` })).toBeVisible();
});
test('Verify that user can search @mobile', async ({ page }) => {
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
await page.goto('./');
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
//Search Results appear in search modal
await expect(page.getByLabel('Object Results').getByText('Parent Display Layout')).toBeVisible();
//Clicking on the search result takes you to the object
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
});

View File

@@ -93,4 +93,14 @@ test.describe('Visual - Display Layout', () => {
await page.getByLabel('Parent Layout Layout', { exact: true }).click();
await percySnapshot(page, `Parent outer layout selected (theme: '${theme}')`);
});
test('Toolbar does not overflow into inspector', async ({ page, theme }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7036'
});
await page.getByLabel('Expand Inspect Pane').click();
await page.getByLabel('Resize Inspect Pane').dragTo(page.getByLabel('X:'));
await percySnapshot(page, `Toolbar does not overflow into inspector (theme: '${theme}')`);
});
});

View File

@@ -47,7 +47,7 @@ if (document.currentScript) {
* @property {*} inspectorViews
* @property {*} propertyEditors
* @property {*} toolbars
* @property {*} types
* @property {import('./src/api/types/TypeRegistry').default} types
* @property {import('./src/api/objects/ObjectAPI').default} objects
* @property {import('./src/api/telemetry/TelemetryAPI').default} telemetry
* @property {import('./src/api/indicators/IndicatorAPI').default} indicators
@@ -67,6 +67,7 @@ if (document.currentScript) {
* @property {import('./src/api/annotation/AnnotationAPI').default} annotation
* @property {{(plugin: OpenMCTPlugin) => void}} install
* @property {{() => string}} getAssetPath
* @property {{(assetPath: string) => void}} setAssetPath
* @property {{(domElement: HTMLElement, isHeadlessMode: boolean) => void}} start
* @property {{() => void}} startHeadless
* @property {{() => void}} destroy

View File

@@ -80,7 +80,7 @@
"tiny-emitter": "2.1.0",
"typescript": "5.3.3",
"uuid": "9.0.1",
"vue": "3.3.8",
"vue": "3.4.19",
"vue-eslint-parser": "9.3.2",
"vue-loader": "16.8.3",
"webpack": "5.89.0",
@@ -107,6 +107,7 @@
"test:debug": "KARMA_DEBUG=true karma start karma.conf.cjs",
"test:e2e": "npx playwright test",
"test:e2e:a11y": "npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep @a11y",
"test:e2e:mobile": "npx playwright test --config=e2e/playwright-mobile.config.js",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"",
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import _ from 'lodash';
import objectUtils from '../objects/object-utils.js';
import { makeKeyString, parseKeyString } from '../objects/object-utils.js';
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
@@ -223,18 +223,18 @@ export default class CompositionProvider {
* @param {DomainObject} oldDomainObject
*/
#onMutation(newDomainObject, oldDomainObject) {
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const id = makeKeyString(oldDomainObject.identifier);
const listeners = this.#listeningTo[id];
if (!listeners) {
return;
}
const oldComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const newComposition = newDomainObject.composition.map(objectUtils.makeKeyString);
const oldComposition = oldDomainObject.composition.map(makeKeyString);
const newComposition = newDomainObject.composition.map(makeKeyString);
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
const added = _.difference(newComposition, oldComposition).map(parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(parseKeyString);
function notify(value) {
return function (listener) {

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import { toRaw } from 'vue';
import objectUtils from '../objects/object-utils.js';
import { makeKeyString } from '../objects/object-utils.js';
import CompositionProvider from './CompositionProvider.js';
/**
@@ -91,7 +91,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
this.establishTopicListener();
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const keyString = makeKeyString(domainObject.identifier);
let objectListeners = this.listeningTo[keyString];
if (!objectListeners) {
@@ -120,7 +120,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
*/
off(domainObject, event, callback, context) {
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const keyString = makeKeyString(domainObject.identifier);
const objectListeners = this.listeningTo[keyString];
const index = objectListeners[event].findIndex((l) => {
@@ -228,7 +228,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
/** @type {string} */
let id = objectUtils.makeKeyString(domainObject.identifier);
let id = makeKeyString(domainObject.identifier);
const listeners = this.listeningTo[id];
if (!listeners) {

View File

@@ -22,7 +22,7 @@
import EventEmitter from 'EventEmitter';
import _ from 'lodash';
import utils from './object-utils.js';
import { makeKeyString, refresh } from './object-utils.js';
const ANY_OBJECT_EVENT = 'mutation';
@@ -152,7 +152,7 @@ class MutableDomainObject {
mutable.$observe('$_synchronize_model', (updatedObject) => {
let clone = JSON.parse(JSON.stringify(updatedObject));
utils.refresh(mutable, clone);
refresh(mutable, clone);
});
return mutable;
@@ -168,7 +168,7 @@ class MutableDomainObject {
}
function qualifiedEventName(object, eventName) {
let keystring = utils.makeKeyString(object.identifier);
let keystring = makeKeyString(object.identifier);
return [keystring, eventName].join(':');
}

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import utils from 'objectUtils';
import { identifierEquals, makeKeyString, parseKeyString, refresh } from 'objectUtils';
import ConflictError from './ConflictError.js';
import InMemorySearchProvider from './InMemorySearchProvider.js';
@@ -82,8 +82,19 @@ import Transaction from './Transaction.js';
* @memberof module:openmct
*/
export default class ObjectAPI {
#makeKeyString;
#parseKeyString;
#identifierEquals;
#refresh;
#openmct;
constructor(typeRegistry, openmct) {
this.openmct = openmct;
this.#makeKeyString = makeKeyString;
this.#parseKeyString = parseKeyString;
this.#identifierEquals = identifierEquals;
this.#refresh = refresh;
this.#openmct = openmct;
this.typeRegistry = typeRegistry;
this.SEARCH_TYPES = Object.freeze({
OBJECTS: 'OBJECTS',
@@ -206,14 +217,14 @@ export default class ObjectAPI {
* has been saved, or be rejected if it cannot be saved
*/
get(identifier, abortSignal, forceRemote = false) {
let keystring = this.makeKeyString(identifier);
let keystring = this.#makeKeyString(identifier);
if (!forceRemote) {
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = utils.parseKeyString(identifier);
identifier = parseKeyString(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
@@ -227,7 +238,7 @@ export default class ObjectAPI {
const provider = this.getProvider(identifier);
if (!provider) {
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}"`);
throw new Error(`No Provider Matched for keyString "${this.#makeKeyString(identifier)}"`);
}
if (!provider.get) {
@@ -325,7 +336,7 @@ export default class ObjectAPI {
*/
getMutable(identifier) {
if (!this.supportsMutation(identifier)) {
throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
throw new Error(`Object "${this.#makeKeyString(identifier)}" does not support mutation.`);
}
return this.get(identifier).then((object) => {
@@ -352,7 +363,7 @@ export default class ObjectAPI {
}
isPersistable(idOrKeyString) {
let identifier = utils.parseKeyString(idOrKeyString);
let identifier = parseKeyString(idOrKeyString);
let provider = this.getProvider(identifier);
if (provider?.isReadOnly) {
return !provider.isReadOnly();
@@ -362,7 +373,7 @@ export default class ObjectAPI {
}
isMissing(domainObject) {
let identifier = utils.makeKeyString(domainObject.identifier);
let identifier = makeKeyString(domainObject.identifier);
let missingName = 'Missing: ' + identifier;
return domainObject.name === missingName;
@@ -442,21 +453,21 @@ export default class ObjectAPI {
if (error instanceof this.errors.Conflict) {
// Synchronized objects will resolve their own conflicts
if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
this.openmct.notifications.info(
`Conflict detected while saving "${this.makeKeyString(
this.#openmct.notifications.info(
`Conflict detected while saving "${this.#makeKeyString(
domainObject.name
)}", attempting to resolve`
);
} else {
this.openmct.notifications.error(
`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`
this.#openmct.notifications.error(
`Conflict detected while saving ${this.#makeKeyString(domainObject.identifier)}`
);
if (this.isTransactionActive()) {
this.endTransaction();
}
await this.refresh(domainObject);
await this.#refresh(domainObject);
}
}
@@ -465,7 +476,7 @@ export default class ObjectAPI {
}
async #getCurrentUsername() {
const user = await this.openmct.user.getCurrentUser();
const user = await this.#openmct.user.getCurrentUser();
let username;
if (user !== undefined) {
@@ -554,7 +565,7 @@ export default class ObjectAPI {
*/
getRelativePath(objectPath) {
return objectPath
.map((p) => this.makeKeyString(p.identifier))
.map((p) => this.#makeKeyString(p.identifier))
.reverse()
.join('/');
}
@@ -574,13 +585,13 @@ export default class ObjectAPI {
}
let sourceTelemetry = null;
if (telemetryIdentifier && utils.identifierEquals(identifier, telemetryIdentifier)) {
if (telemetryIdentifier && this.#identifierEquals(identifier, telemetryIdentifier)) {
sourceTelemetry = identifier;
} else if (objectDetails.composition) {
sourceTelemetry = objectDetails.composition[0];
if (telemetryIdentifier) {
sourceTelemetry = objectDetails.composition.find((telemetrySource) =>
utils.identifierEquals(telemetrySource, telemetryIdentifier)
this.#identifierEquals(telemetrySource, telemetryIdentifier)
);
}
}
@@ -666,7 +677,7 @@ export default class ObjectAPI {
mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter);
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
let identifier = parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier);
if (
@@ -706,7 +717,7 @@ export default class ObjectAPI {
if (domainObject.isMutable) {
domainObject.$refresh(refreshedObject);
} else {
utils.refresh(domainObject, refreshedObject);
refresh(domainObject, refreshedObject);
}
return domainObject;
@@ -745,7 +756,7 @@ export default class ObjectAPI {
* @returns {string} A string representation of the given identifier, including namespace and key
*/
makeKeyString(identifier) {
return utils.makeKeyString(identifier);
return makeKeyString(identifier);
}
/**
@@ -753,7 +764,7 @@ export default class ObjectAPI {
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object
*/
parseKeyString(keyString) {
return utils.parseKeyString(keyString);
return parseKeyString(keyString);
}
/**
@@ -761,9 +772,9 @@ export default class ObjectAPI {
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
*/
areIdsEqual(...identifiers) {
const firstIdentifier = utils.parseKeyString(identifiers[0]);
const firstIdentifier = this.#parseKeyString(identifiers[0]);
return identifiers.map(utils.parseKeyString).every((identifier) => {
return identifiers.map(this.#parseKeyString).every((identifier) => {
return (
identifier === firstIdentifier ||
(identifier.namespace === firstIdentifier.namespace &&
@@ -791,7 +802,7 @@ export default class ObjectAPI {
}
return path.some((pathElement) => {
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
const identifierToCheck = this.#parseKeyString(keyStringToCheck);
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
});
@@ -814,7 +825,7 @@ export default class ObjectAPI {
if (location && !this.#pathContainsDomainObject(location, path)) {
// if we have a location, and we don't already have this in our constructed path,
// then keep walking up the path
return this.getOriginalPath(utils.parseKeyString(location), path, abortSignal);
return this.getOriginalPath(this.#parseKeyString(location), path, abortSignal);
} else {
return path;
}
@@ -853,8 +864,8 @@ export default class ObjectAPI {
await Promise.all(
keyStrings.map((keyString) =>
this.supportsMutation(keyString)
? this.getMutable(utils.parseKeyString(keyString))
: this.get(utils.parseKeyString(keyString))
? this.getMutable(this.#parseKeyString(keyString))
: this.get(this.#parseKeyString(keyString))
)
)
).reverse();
@@ -866,7 +877,7 @@ export default class ObjectAPI {
return (
objectPath !== undefined &&
objectPath.length > 1 &&
domainObject.location !== this.makeKeyString(objectPath[1].identifier)
domainObject.location !== this.#makeKeyString(objectPath[1].identifier)
);
}

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import utils from './object-utils.js';
import { isIdentifier } from './object-utils.js';
export default class RootRegistry {
constructor(openmct) {
@@ -47,12 +47,12 @@ export default class RootRegistry {
}
_isValid(rootItem) {
if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') {
if (isIdentifier(rootItem) || typeof rootItem === 'function') {
return true;
}
if (Array.isArray(rootItem)) {
return rootItem.every(utils.isIdentifier);
return rootItem.every(isIdentifier);
}
return false;

View File

@@ -1,4 +1,4 @@
import utils from 'objectUtils';
import { makeKeyString, parseKeyString } from 'objectUtils';
import Transaction from './Transaction.js';
@@ -9,7 +9,7 @@ let transaction;
describe('Transaction Class', () => {
beforeEach(() => {
objectAPI = {
makeKeyString: (identifier) => utils.makeKeyString(identifier),
makeKeyString: (identifier) => makeKeyString(identifier),
save: () => Promise.resolve(true),
mutate: (object, prop, value) => {
object[prop] = value;
@@ -18,7 +18,7 @@ describe('Transaction Class', () => {
},
refresh: (object) => Promise.resolve(object),
areIdsEqual: (...identifiers) => {
return identifiers.map(utils.parseKeyString).every((identifier) => {
return identifiers.map(parseKeyString).every((identifier) => {
return (
identifier === identifiers[0] ||
(identifier.namespace === identifiers[0].namespace &&

View File

@@ -24,7 +24,7 @@
* Utility for checking if a thing is an Open MCT Identifier.
* @private
*/
function isIdentifier(thing) {
export function isIdentifier(thing) {
return (
typeof thing === 'object' &&
Object.prototype.hasOwnProperty.call(thing, 'key') &&
@@ -36,7 +36,7 @@ function isIdentifier(thing) {
* Utility for checking if a thing is a key string. Not perfect.
* @private
*/
function isKeyString(thing) {
export function isKeyString(thing) {
return typeof thing === 'string';
}
@@ -49,7 +49,7 @@ function isKeyString(thing) {
* @param keyString
* @returns identifier
*/
function parseKeyString(keyString) {
export function parseKeyString(keyString) {
if (isIdentifier(keyString)) {
return keyString;
}
@@ -86,7 +86,7 @@ function parseKeyString(keyString) {
* @param identifier
* @returns keyString
*/
function makeKeyString(identifier) {
export function makeKeyString(identifier) {
if (!identifier) {
throw new Error('Cannot make key string from null identifier');
}
@@ -112,7 +112,7 @@ function makeKeyString(identifier) {
* @param domainObject
* @returns oldFormatModel
*/
function toOldFormat(model) {
export function toOldFormat(model) {
model = JSON.parse(JSON.stringify(model));
delete model.identifier;
if (model.composition) {
@@ -131,7 +131,7 @@ function toOldFormat(model) {
* @param keyString
* @returns domainObject
*/
function toNewFormat(model, keyString) {
export function toNewFormat(model, keyString) {
model = JSON.parse(JSON.stringify(model));
model.identifier = parseKeyString(keyString);
if (model.composition) {
@@ -148,7 +148,7 @@ function toNewFormat(model, keyString) {
* @param otherIdentifier
* @returns Boolean true if identifiers are equal.
*/
function identifierEquals(a, b) {
export function identifierEquals(a, b) {
return a.key === b.key && a.namespace === b.namespace;
}
@@ -160,23 +160,12 @@ function identifierEquals(a, b) {
* @param otherDomainOBject
* @returns Boolean true if objects are equal.
*/
function objectEquals(a, b) {
export function objectEquals(a, b) {
return identifierEquals(a.identifier, b.identifier);
}
function refresh(oldObject, newObject) {
export function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
}
export default {
isIdentifier: isIdentifier,
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString,
equals: objectEquals,
identifierEquals: identifierEquals,
refresh: refresh
};

View File

@@ -1,4 +1,4 @@
import objectUtils from 'objectUtils';
import { makeKeyString, parseKeyString, toNewFormat, toOldFormat } from 'objectUtils';
describe('objectUtils', function () {
describe('keyString util', function () {
@@ -31,27 +31,27 @@ describe('objectUtils', function () {
Object.keys(EXPECTATIONS).forEach(function (keyString) {
it('parses "' + keyString + '".', function () {
expect(objectUtils.parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
expect(parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
});
it('parses and re-encodes "' + keyString + '"', function () {
const identifier = objectUtils.parseKeyString(keyString);
expect(objectUtils.makeKeyString(identifier)).toEqual(keyString);
const identifier = parseKeyString(keyString);
expect(makeKeyString(identifier)).toEqual(keyString);
});
it('is idempotent for "' + keyString + '".', function () {
const identifier = objectUtils.parseKeyString(keyString);
let again = objectUtils.parseKeyString(identifier);
const identifier = parseKeyString(keyString);
let again = parseKeyString(identifier);
expect(identifier).toEqual(again);
again = objectUtils.parseKeyString(again);
again = objectUtils.parseKeyString(again);
again = parseKeyString(again);
again = parseKeyString(again);
expect(identifier).toEqual(again);
let againKeyString = objectUtils.makeKeyString(again);
let againKeyString = makeKeyString(again);
expect(againKeyString).toEqual(keyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = makeKeyString(againKeyString);
againKeyString = makeKeyString(againKeyString);
againKeyString = makeKeyString(againKeyString);
expect(againKeyString).toEqual(keyString);
});
});
@@ -60,7 +60,7 @@ describe('objectUtils', function () {
describe('old object conversions', function () {
it('translate ids', function () {
expect(
objectUtils.toNewFormat(
toNewFormat(
{
prop: 'someValue'
},
@@ -77,7 +77,7 @@ describe('objectUtils', function () {
it('translates composition', function () {
expect(
objectUtils.toNewFormat(
toNewFormat(
{
prop: 'someValue',
composition: ['anotherObjectId', 'scratch:anotherObjectId']
@@ -107,7 +107,7 @@ describe('objectUtils', function () {
describe('new object conversions', function () {
it('removes ids', function () {
expect(
objectUtils.toOldFormat({
toOldFormat({
prop: 'someValue',
identifier: {
namespace: '',
@@ -121,7 +121,7 @@ describe('objectUtils', function () {
it('translates composition', function () {
expect(
objectUtils.toOldFormat({
toOldFormat({
prop: 'someValue',
composition: [
{

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter.js';
import BatchingWebSocket from './BatchingWebSocket.js';
@@ -429,7 +429,7 @@ export default class TelemetryAPI {
this.#subscribeCache = {};
}
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const keyString = makeKeyString(domainObject.identifier);
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
// Override the requested strategy with the strategy supported by the provider
const optionsWithSupportedStrategy = {
@@ -541,7 +541,7 @@ export default class TelemetryAPI {
this.stalenessSubscriberCache = {};
}
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const keyString = makeKeyString(domainObject.identifier);
let stalenessSubscriber = this.stalenessSubscriberCache[keyString];
if (!stalenessSubscriber) {
@@ -600,7 +600,7 @@ export default class TelemetryAPI {
this.limitsSubscribeCache = {};
}
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const keyString = makeKeyString(domainObject.identifier);
let subscriber = this.limitsSubscribeCache[keyString];
if (!subscriber) {

View File

@@ -31,10 +31,10 @@
<div class="c-cs__header-label c-section__label">Test Data</div>
</div>
<div v-if="expanded" class="c-cs__content">
<div class="c-cs__test-data__controls c-cdef__controls" :disabled="!telemetry.length">
<div :class="['c-cs__test-data__controls c-cdef__controls', { disabled: !telemetry.length }]">
<label class="c-toggle-switch">
<input type="checkbox" :checked="isApplied" @change="applyTestData" />
<span class="c-toggle-switch__slider"></span>
<span class="c-toggle-switch__slider" aria-label="Apply Test Data"></span>
<span class="c-toggle-switch__label">Apply Test Data</span>
</label>
</div>
@@ -47,7 +47,11 @@
<span class="c-cs-test__label">Set</span>
<span class="c-cs-test__controls">
<span class="c-cdef__control">
<select v-model="testInput.telemetry" @change="updateMetadata(testInput)">
<select
v-model="testInput.telemetry"
aria-label="Test Data Telemetry Selection"
@change="updateMetadata(testInput)"
>
<option value="">- Select Telemetry -</option>
<option
v-for="(telemetryOption, index) in telemetry"
@@ -59,7 +63,11 @@
</select>
</span>
<span v-if="testInput.telemetry" class="c-cdef__control">
<select v-model="testInput.metadata" @change="updateTestData">
<select
v-model="testInput.metadata"
aria-label="Test Data Metadata Selection"
@change="updateTestData"
>
<option value="">- Select Field -</option>
<option
v-for="(option, index) in telemetryMetadataOptions[getId(testInput.telemetry)]"
@@ -76,6 +84,7 @@
placeholder="Enter test input"
type="text"
class="c-cdef__control__input"
aria-label="Test Data Input"
@change="updateTestData"
/>
</span>

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from 'objectUtils';
import { parseKeyString } from 'objectUtils';
import { filter__proto__ } from 'utils/sanitization';
import { v4 as uuid } from 'uuid';
@@ -158,7 +158,7 @@ export default class ImportAsJSONAction {
key: uuid()
};
const oldId = objectUtils.parseKeyString(domainObjectId);
const oldId = parseKeyString(domainObjectId);
tree = this._rewriteId(oldId, newId, tree);
}, this);

View File

@@ -23,6 +23,8 @@
<template>
<li
draggable="true"
:aria-label="`${elementObject.name} Element Item`"
:aria-grabbed="hover"
@dragstart="emitDragStartEvent"
@dragenter="onDragenter"
@dragover.prevent
@@ -76,16 +78,19 @@ export default {
},
emits: ['drop-custom', 'dragstart-custom'],
data() {
const isAlias =
this.elementObject.location !==
this.openmct.objects.makeKeyString(this.domainObject.identifier);
return {
contextClickActive: false,
hover: false,
isAlias
hover: false
};
},
computed: {
isAlias() {
return (
this.elementObject.location !==
this.openmct.objects.makeKeyString(this.domainObject.identifier)
);
}
},
methods: {
emitDropEvent(event) {
this.$emit('drop-custom', event);

View File

@@ -30,7 +30,7 @@
/>
<div class="c-elements-pool__elements">
<ul
v-if="hasElements"
v-show="hasElements"
id="inspector-elements-tree"
class="c-tree c-elements-pool__tree js-elements-pool__tree"
>
@@ -63,7 +63,7 @@
></li>
</element-item-group>
</ul>
<div v-if="!hasElements">No contained elements</div>
<div v-show="!hasElements">No contained elements</div>
</div>
</div>
</template>
@@ -169,7 +169,8 @@ export default {
setYAxisIds() {
const configId = this.openmct.objects.makeKeyString(this.parentObject.identifier);
this.config = configStore.get(configId);
this.yAxes = [];
// Clear the yAxes array and repopulate it with the current YAxis elements
this.yAxes.splice(0);
this.yAxes.push({
id: this.config.yAxis.id,
elements: this.parentObject.configuration.series.filter(
@@ -207,7 +208,7 @@ export default {
}
// Store the element in the cache and set its yAxisId
this.elementsCache[keyString] = JSON.parse(JSON.stringify(element));
this.elementsCache[keyString] = element;
if (this.elementsCache[keyString].yAxisId !== yAxisId) {
// Mutate the YAxisId on the domainObject itself
this.updateCacheAndMutate(element, yAxisId);
@@ -276,7 +277,7 @@ export default {
yAxisId
});
this.composition.add(domainObject);
this.elementsCache[keyString] = JSON.parse(JSON.stringify(domainObject));
this.elementsCache[keyString] = domainObject;
}
this.elementsCache[keyString].yAxisId = yAxisId;

View File

@@ -1,4 +1,4 @@
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
const NOTEBOOK_LOCAL_STORAGE = 'notebook-storage';
let currentNotebookObjectIdentifier = null;
@@ -22,8 +22,7 @@ function defaultNotebookObjectChanged(newDomainObject) {
function observeDefaultNotebookObject(openmct, notebookStorage, domainObject) {
if (
currentNotebookObjectIdentifier &&
objectUtils.makeKeyString(currentNotebookObjectIdentifier) ===
objectUtils.makeKeyString(notebookStorage.identifier)
makeKeyString(currentNotebookObjectIdentifier) === makeKeyString(notebookStorage.identifier)
) {
return;
}

View File

@@ -129,7 +129,7 @@ export default {
this.role = activeRole;
const status = await this.openmct.user.status.getStatusForRole(activeRole);
if (status !== undefined) {
this.setStatus({ status });
this.setStatus({ role: this.role, status });
}
},
subscribeToMyStatus() {
@@ -141,7 +141,11 @@ export default {
subscribeToRoleChange() {
this.openmct.user.on('roleChanged', this.fetchMyStatus);
},
setStatus({ status }) {
setStatus({ role, status }) {
if (role !== this.role) {
// not my role
return;
}
status = this.applyStyling(status);
this.selectedStatus = status.key;
this.indicator.iconClass(status.iconClassPoll);

View File

@@ -26,7 +26,7 @@
* rootIdentifier, and rewrites all child object identifiers so that they
* exist in the same namespace as the rootIdentifier.
*/
import objectUtils from 'objectUtils';
import { makeKeyString, parseKeyString, toNewFormat } from 'objectUtils';
class StaticModelProvider {
constructor(importData, rootIdentifier) {
@@ -38,7 +38,7 @@ class StaticModelProvider {
* Standard "Get".
*/
get(identifier) {
const keyString = objectUtils.makeKeyString(identifier);
const keyString = makeKeyString(identifier);
if (this.objectMap[keyString]) {
return this.objectMap[keyString];
}
@@ -49,7 +49,7 @@ class StaticModelProvider {
parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) {
Object.keys(objectLeaf).forEach((nodeKey) => {
if (idMap.get(nodeKey)) {
const newIdentifier = objectUtils.makeKeyString({
const newIdentifier = makeKeyString({
namespace: newRootNamespace,
key: idMap.get(nodeKey)
});
@@ -104,7 +104,7 @@ class StaticModelProvider {
let mappedLeafValue;
if (oldRootNamespace) {
mappedLeafValue = idMap.get(
objectUtils.makeKeyString({
makeKeyString({
namespace: oldRootNamespace,
key: leafValue
})
@@ -125,7 +125,7 @@ class StaticModelProvider {
return null;
}
const newLocationIdentifier = objectUtils.makeKeyString({
const newLocationIdentifier = makeKeyString({
namespace: newRootNamespace,
key: mappedLeafValue
});
@@ -134,7 +134,7 @@ class StaticModelProvider {
} else {
const mappedLeafValue = idMap.get(leafValue);
if (mappedLeafValue) {
const newIdentifier = objectUtils.makeKeyString({
const newIdentifier = makeKeyString({
namespace: newRootNamespace,
key: mappedLeafValue
});
@@ -147,7 +147,7 @@ class StaticModelProvider {
}
rewriteObjectIdentifiers(importData, rootIdentifier) {
const { namespace: oldRootNamespace } = objectUtils.parseKeyString(importData.rootId);
const { namespace: oldRootNamespace } = parseKeyString(importData.rootId);
const { namespace: newRootNamespace } = rootIdentifier;
const idMap = new Map();
const objectTree = importData.openmct;
@@ -172,7 +172,7 @@ class StaticModelProvider {
*/
convertToNewObjects(oldObjectMap) {
return Object.keys(oldObjectMap).reduce(function (newObjectMap, key) {
newObjectMap[key] = objectUtils.toNewFormat(oldObjectMap[key], key);
newObjectMap[key] = toNewFormat(oldObjectMap[key], key);
return newObjectMap;
}, {});
@@ -180,7 +180,7 @@ class StaticModelProvider {
/* Set the root location correctly for a top-level object */
setRootLocation(objectMap, rootIdentifier) {
objectMap[objectUtils.makeKeyString(rootIdentifier)].location = 'ROOT';
objectMap[makeKeyString(rootIdentifier)].location = 'ROOT';
return objectMap;
}

View File

@@ -1,6 +1,6 @@
import EventEmitter from 'EventEmitter';
import _ from 'lodash';
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
import ConditionEvaluator from './ConditionEvaluator.js';
@@ -119,7 +119,7 @@ ConditionManager.prototype.addGlobalPropertyType = function (key, type) {
* has completed and types have been parsed
*/
ConditionManager.prototype.parsePropertyTypes = function (object) {
const objectId = objectUtils.makeKeyString(object.identifier);
const objectId = makeKeyString(object.identifier);
this.telemetryTypesById[objectId] = {};
Object.values(this.telemetryMetadataById[objectId]).forEach(function (valueMetadata) {
@@ -182,7 +182,7 @@ ConditionManager.prototype.createNormalizedDatum = function (objId, telemetryDat
ConditionManager.prototype.onCompositionAdd = function (obj) {
let compositionKeys;
const telemetryAPI = this.openmct.telemetry;
const objId = objectUtils.makeKeyString(obj.identifier);
const objId = makeKeyString(obj.identifier);
let telemetryMetadata;
const self = this;
@@ -191,7 +191,7 @@ ConditionManager.prototype.onCompositionAdd = function (obj) {
self.telemetryMetadataById[objId] = {};
// FIXME: this should just update based on listener.
compositionKeys = self.domainObject.composition.map(objectUtils.makeKeyString);
compositionKeys = self.domainObject.composition.map(makeKeyString);
if (!compositionKeys.includes(objId)) {
self.domainObject.composition.push(obj.identifier);
}
@@ -245,7 +245,7 @@ ConditionManager.prototype.onCompositionAdd = function (obj) {
* @private
*/
ConditionManager.prototype.onCompositionRemove = function (identifier) {
const objectId = objectUtils.makeKeyString(identifier);
const objectId = makeKeyString(identifier);
// FIXME: this should just update by listener.
_.remove(this.domainObject.composition, function (id) {
return id.key === identifier.key && id.namespace === identifier.namespace;

View File

@@ -1,4 +1,4 @@
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
import Select from './Select.js';
@@ -39,7 +39,7 @@ export default function ObjectSelect(config, manager, baseOptions) {
* @private
*/
function onCompositionAdd(obj) {
self.select.addOption(objectUtils.makeKeyString(obj.identifier), obj.name);
self.select.addOption(makeKeyString(obj.identifier), obj.name);
}
/**
@@ -77,7 +77,7 @@ export default function ObjectSelect(config, manager, baseOptions) {
*/
ObjectSelect.prototype.generateOptions = function () {
const items = Object.values(this.compositionObjs).map(function (obj) {
return [objectUtils.makeKeyString(obj.identifier), obj.name];
return [makeKeyString(obj.identifier), obj.name];
});
this.baseOptions.forEach(function (option, index) {
items.splice(index, 0, option);

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
import SummaryWidgetEvaluator from './SummaryWidgetEvaluator.js';
@@ -31,7 +31,7 @@ export default function EvaluatorPool(openmct) {
}
EvaluatorPool.prototype.get = function (domainObject) {
const objectId = objectUtils.makeKeyString(domainObject.identifier);
const objectId = makeKeyString(domainObject.identifier);
let poolEntry = this.byObjectId[objectId];
if (!poolEntry) {
poolEntry = {

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import _ from 'lodash';
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
import eventHelpers from '../eventHelpers.js';
import SummaryWidgetRule from './SummaryWidgetRule.js';
@@ -113,7 +113,7 @@ SummaryWidgetEvaluator.prototype.updateRules = function (domainObject) {
};
SummaryWidgetEvaluator.prototype.addChild = function (childObject) {
const childId = objectUtils.makeKeyString(childObject.identifier);
const childId = makeKeyString(childObject.identifier);
const metadata = this.openmct.telemetry.getMetadata(childObject);
const formats = this.openmct.telemetry.getFormatMap(metadata);
@@ -126,7 +126,7 @@ SummaryWidgetEvaluator.prototype.addChild = function (childObject) {
};
SummaryWidgetEvaluator.prototype.removeChild = function (childObject) {
const childId = objectUtils.makeKeyString(childObject.identifier);
const childId = makeKeyString(childObject.identifier);
delete this.baseState[childId];
};

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from 'objectUtils';
import { parseKeyString } from 'objectUtils';
import TelemetryAverager from './TelemetryAverager.js';
@@ -43,7 +43,7 @@ MeanTelemetryProvider.prototype.supportsRequest =
MeanTelemetryProvider.prototype.subscribe = function (domainObject, callback) {
let wrappedUnsubscribe;
let unsubscribeCalled = false;
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
const objectId = parseKeyString(domainObject.telemetryPoint);
const samples = domainObject.samples;
this.objectAPI
@@ -79,7 +79,7 @@ MeanTelemetryProvider.prototype.subscribeToAverage = function (domainObject, sam
};
MeanTelemetryProvider.prototype.request = function (domainObject, request) {
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
const objectId = parseKeyString(domainObject.telemetryPoint);
const samples = domainObject.samples;
return this.objectAPI.get(objectId).then(
@@ -119,7 +119,7 @@ MeanTelemetryProvider.prototype.requestAverageTelemetry = function (
* @private
*/
MeanTelemetryProvider.prototype.getLinkedObject = function (domainObject) {
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
const objectId = parseKeyString(domainObject.telemetryPoint);
return this.objectAPI.get(objectId);
};

View File

@@ -85,8 +85,6 @@ import { getFilteredValues, getValidatedData, getValidatedGroups } from '../plan
import { SORT_ORDER_OPTIONS } from './constants.js';
import ExpandedViewItem from './ExpandedViewItem.vue';
const SCROLL_TIMEOUT = 10000;
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
const SAME_DAY_PRECISION_SECONDS = 'HH:mm:ss';
@@ -209,7 +207,6 @@ export default {
},
created() {
this.updateTimestamp = _.throttle(this.updateTimestamp, 1000);
this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500);
this.setTimeContext();
this.timestamp = this.timeContext.now();
@@ -238,8 +235,6 @@ export default {
this.openmct.editor.on('isEditing', this.setEditState);
this.$el.parentElement.addEventListener('scroll', this.deferAutoScroll, true);
if (this.composition) {
this.composition.on('add', this.addToComposition);
this.composition.on('remove', this.removeItem);
@@ -272,11 +267,6 @@ export default {
this.openmct.editor.off('isEditing', this.setEditState);
this.stopFollowingTimeContext();
this.$el.parentElement?.removeEventListener('scroll', this.deferAutoScroll, true);
if (this.clearAutoScrollDisabledTimer) {
clearTimeout(this.clearAutoScrollDisabledTimer);
}
if (this.composition) {
this.composition.off('add', this.addToComposition);
this.composition.off('remove', this.removeItem);
@@ -429,8 +419,6 @@ export default {
const filteredItems = this.planActivities.filter(this.filterActivities);
const sortedItems = this.sortItems(filteredItems);
this.sortedItems = this.applyStyles(sortedItems);
//We need to wait for the next tick since we need the height of the row from the DOM
this.$nextTick(this.setScrollTop);
},
updateTimeStampAndListActivities(time) {
this.timestamp = time;
@@ -509,24 +497,13 @@ export default {
});
},
// Add activity classes, increase activity counts by type,
// set indices of the first occurrences of current and future activities - used for scrolling
styleActivity(activity, index) {
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
activity.cssClass = CURRENT_CSS_SUFFIX;
if (this.firstCurrentActivityIndex < 0) {
this.firstCurrentActivityIndex = index;
}
this.currentActivitiesCount = this.currentActivitiesCount + 1;
} else if (this.timestamp < activity.start) {
activity.cssClass = FUTURE_CSS_SUFFIX;
//the index of the first activity that's greater than the current timestamp
if (this.firstFutureActivityIndex < 0) {
this.firstFutureActivityIndex = index;
}
this.futureActivitiesCount = this.futureActivitiesCount + 1;
} else {
activity.cssClass = PAST_CSS_SUFFIX;
this.pastActivitiesCount = this.pastActivitiesCount + 1;
}
if (!activity.key) {
@@ -545,111 +522,7 @@ export default {
return activity;
},
applyStyles(activities) {
this.firstCurrentOrFutureActivityIndex = -1;
this.firstCurrentActivityIndex = -1;
this.firstFutureActivityIndex = -1;
this.currentActivitiesCount = 0;
this.pastActivitiesCount = 0;
this.futureActivitiesCount = 0;
const styledActivities = activities.map(this.styleActivity);
if (this.firstCurrentActivityIndex > -1) {
this.firstCurrentOrFutureActivityIndex = this.firstCurrentActivityIndex;
} else if (this.firstFutureActivityIndex > -1) {
this.firstCurrentOrFutureActivityIndex = this.firstFutureActivityIndex;
}
return styledActivities;
},
canAutoScroll() {
//this distinguishes between programmatic vs user-triggered scroll events
this.autoScrolled = this.dontAutoScroll !== true;
return this.autoScrolled;
},
resetScroll() {
if (this.canAutoScroll() === false) {
return;
}
this.firstCurrentOrFutureActivityIndex = -1;
this.pastActivitiesCount = 0;
this.currentActivitiesCount = 0;
this.futureActivitiesCount = 0;
this.$el.parentElement?.scrollTo({ top: 0 });
this.autoScrolled = false;
},
setScrollTop() {
//The view isn't ready yet
if (!this.$el.parentElement || this.isExpanded) {
return;
}
if (this.canAutoScroll() === false) {
return;
}
// See #7167 for scrolling algorithm
const scrollTop = this.calculateScrollOffset();
if (scrollTop === undefined) {
this.resetScroll();
} else {
this.$el.parentElement?.scrollTo({
top: scrollTop,
behavior: 'smooth'
});
this.autoScrolled = false;
}
},
calculateScrollOffset() {
let scrollTop;
//No scrolling necessary if no past events are present
if (this.pastActivitiesCount > 0) {
const row = this.$el.querySelector('.js-list-item');
const ROW_HEIGHT = row.getBoundingClientRect().height;
const maxViewableActivities =
Math.floor(this.$el.parentElement.getBoundingClientRect().height / ROW_HEIGHT) - 1;
const currentAndFutureActivities = this.currentActivitiesCount + this.futureActivitiesCount;
//If there is more viewable area than all current and future activities combined, then show some past events
const numberOfPastEventsToShow = maxViewableActivities - currentAndFutureActivities;
if (numberOfPastEventsToShow > 0) {
//some past events can be shown - get that scroll index
if (this.pastActivitiesCount > numberOfPastEventsToShow) {
scrollTop =
ROW_HEIGHT * (this.firstCurrentOrFutureActivityIndex + numberOfPastEventsToShow);
}
} else {
// only show current and future events
scrollTop = ROW_HEIGHT * this.firstCurrentOrFutureActivityIndex;
}
}
return scrollTop;
},
deferAutoScroll() {
//if this is not a user-triggered event, don't defer auto scrolling
if (this.autoScrolled) {
this.autoScrolled = false;
return;
}
this.dontAutoScroll = true;
const self = this;
if (this.clearAutoScrollDisabledTimer) {
clearTimeout(this.clearAutoScrollDisabledTimer);
}
this.clearAutoScrollDisabledTimer = setTimeout(() => {
self.dontAutoScroll = false;
self.setScrollTop();
}, SCROLL_TIMEOUT);
return activities.map(this.styleActivity);
},
setSort() {
const sortOrder = SORT_ORDER_OPTIONS[this.domainObject.configuration.sortOrderIndex];

View File

@@ -25,6 +25,7 @@
class="c-tree__item__label c-object-label"
:class="[statusClass]"
draggable="true"
:aria-label="ariaLabel"
@dragstart="dragStart"
@click="navigateOrPreview"
>
@@ -47,7 +48,10 @@
</template>
<script>
import { inject } from 'vue';
import tooltipHelpers from '../../api/tooltips/tooltipMixins.js';
import { useIsEditing } from '../../ui/composables/edit.js';
import ContextMenuGesture from '../mixins/context-menu-gesture.js';
import ObjectLink from '../mixins/object-link.js';
import PreviewAction from '../preview/PreviewAction.js';
@@ -76,6 +80,13 @@ export default {
}
}
},
setup() {
const openmct = inject('openmct');
const { isEditing } = useIsEditing(openmct);
return {
isEditing
};
},
data() {
return {
status: ''
@@ -92,6 +103,9 @@ export default {
},
statusClass() {
return this.status ? `is-status--${this.status}` : '';
},
ariaLabel() {
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${this.domainObject.type} Object`;
}
},
mounted() {

View File

@@ -33,7 +33,7 @@ import StyleRuleManager from '@/plugins/condition/StyleRuleManager';
import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import objectUtils from '../../api/objects/object-utils.js';
import { objectEquals } from '../../api/objects/object-utils.js';
import VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';
export default {
@@ -224,7 +224,7 @@ export default {
this.updateView(true);
},
reload(domainObjectToReload) {
if (objectUtils.equals(domainObjectToReload, this.domainObject)) {
if (objectEquals(domainObjectToReload, this.domainObject)) {
this.updateView(true);
this.initObjectStyles();
this.triggerStalenessSubscribe(this.domainObject);

View File

@@ -12,6 +12,9 @@
&:before {
// Mag glass icon
content: $glyph-icon-magnify;
body.mobile & { // Make search icon stand out in mobile
opacity: 1;
}
}
&__use-regex {
@@ -47,15 +50,26 @@
display: none;
order: 99;
padding: 1px 0;
body.mobile & {
display: block;
}
}
&.is-active {
body.mobile & { // In mobile, persist the expanded search bar instead of collapsing upon clicking away
background-color: rgba($colorHeadFg, 0.2) !important;
width: 50vw !important;
}
.c-search__use-regex {
margin-left: 0;
}
&:before {
width: 0;
body.mobile & {
width: auto;
}
}
input[type='text'],

View File

@@ -0,0 +1,43 @@
/*****************************************************************************
* 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 { ref } from 'vue';
import { useEventEmitter } from './event.js';
/**
* Provides a reactive `isEditing` property that reflects the current editing state of the
* application.
* @param {OpenMCT} openmct the Open MCT API
* @returns {{
* isEditing: import('vue').Ref<boolean>
* }}
*/
export function useIsEditing(openmct) {
const isEditing = ref(openmct.editor.isEditing());
useEventEmitter(openmct.editor, 'isEditing', (_isEditing) => {
isEditing.value = _isEditing;
});
return { isEditing };
}

View File

@@ -105,7 +105,13 @@
class="l-shell__tree"
/>
</pane>
<pane handle="before" label="Recently Viewed" :persist-position="true">
<pane
handle="before"
label="Recently Viewed"
:persist-position="true"
collapse-type="horizontal"
hide-param="hideRecents"
>
<RecentObjectsList
ref="recentObjectsList"
class="l-shell__tree"

View File

@@ -21,7 +21,13 @@
-->
<template>
<div class="l-pane" :class="paneClasses">
<div v-if="handle" class="l-pane__handle" @mousedown.prevent="startResizing"></div>
<div
v-if="handle"
class="l-pane__handle"
:aria-label="handleLabel"
:aria-grabbed="resizing"
@mousedown.prevent="startResizing"
></div>
<div class="l-pane__header">
<span v-if="label" class="l-pane__label">{{ label }}</span>
<slot name="controls"></slot>
@@ -56,6 +62,10 @@ const LOCAL_STORAGE_KEY__PANE_POSITIONS = 'mct-pane-positions';
export default {
inject: ['openmct'],
props: {
collapseType: {
type: String,
default: 'vertical'
},
handle: {
type: String,
default: '',
@@ -84,6 +94,9 @@ export default {
};
},
computed: {
handleLabel() {
return `Resize ${this.label} Pane`;
},
isCollapsable() {
return this.hideParam?.length > 0;
},
@@ -107,6 +120,7 @@ export default {
'l-pane--vertical-handle-before': this.type === 'vertical' && this.handle === 'before',
'l-pane--vertical-handle-after': this.type === 'vertical' && this.handle === 'after',
'l-pane--collapsed': this.collapsed,
'collapse-horizontal': this.collapseType === 'horizontal',
'l-pane--reacts': !this.handle,
'l-pane--resizing': this.resizing === true
};
@@ -177,7 +191,9 @@ export default {
this.collapsed = true;
},
handleExpand() {
this.$el.style[this.styleProp] = this.currentSize;
let size = this.currentSize ? this.currentSize : this.getSavedPosition();
this.$el.style[this.styleProp] = size;
delete this.currentSize;
delete this.dragCollapse;
this.collapsed = false;

View File

@@ -171,7 +171,7 @@
}
}
&__head,
//&__head,
&__pane-inspector {
body.mobile & {
display: none;
@@ -189,6 +189,18 @@
margin-bottom: $interiorMargin; // Needs some additional visual separation
}
&__head {
body.mobile & {
.c-create-button,
.c-create-menu,
.c-indicator,
.c-icon-button {
// Making status area visible, but hiding some widgets for mobile.
display: none;
}
}
}
body.mobile & .l-shell__main-view-browse-bar {
margin-left: $mobileMenuIconD; // Make room for the hamburger!
.c-button[class*='__actions__edit'] {
@@ -241,7 +253,20 @@
border-left: $brdr;
align-items: start;
$p: $interiorMarginSm;
padding-left: $p; padding-right: $p;
padding-left: $p;
padding-right: $p;
}
&__create-button,
&__app-logo {
flex: 0 0 auto;
}
&__create-button {
body.mobile & {
// Fixes weird gap in mobile status area
margin-right: 0px;
}
}
&__indicators {
@@ -254,7 +279,6 @@
height: 24px;
overflow: hidden;
}
}
/******************************* MAIN AREA */
@@ -355,14 +379,18 @@
.is-editing {
.l-shell__main-container {
$m: 3px;
box-shadow: $colorBodyBg 0 0 0 1px, $editUIAreaShdw;
box-shadow:
$colorBodyBg 0 0 0 1px,
$editUIAreaShdw;
margin-left: $m;
margin-right: $m;
top: $shellToolBarH + $shellMainBrowseBarH + $interiorMarginLg !important;
&[s-selected] {
// Provide a clearer selection context articulation for the main edit area
box-shadow: $colorBodyBg 0 0 0 1px, $editUIAreaShdwSelected;
box-shadow:
$colorBodyBg 0 0 0 1px,
$editUIAreaShdwSelected;
}
}
}

View File

@@ -60,7 +60,7 @@
transition: opacity 150ms ease;
opacity: 0;
pointer-events: none;
overflow: hidden;
overflow: hidden; // Prevents toolbar from extending into Inspector
> * {
min-width: 0 !important;
@@ -93,6 +93,7 @@
&__contents {
flex: 1 1 100%;
opacity: 1;
overflow: hidden;
pointer-events: inherit;
transition: opacity 250ms ease 250ms;
@@ -146,7 +147,9 @@
&:before {
// '+' icon
font-size: 0.8em;
margin-bottom: $interiorMarginSm;
margin-bottom: $interiorMarginSm; // margin-bottom is needed for Tree and Inspector
margin-right: $interiorMarginSm; // margin-right and margin-left are needed for Recent Objects
margin-left: $interiorMarginSm;
}
&:hover {
@@ -191,6 +194,36 @@
}
}
&[class*='--collapsed'] { // For Recent Objects Button
&.collapse-horizontal {
[class*='expand-button'] {
display: block;
position: absolute;
top: 0;
width: 100%;
border-top-right-radius: $controlCr;
border-top-left-radius: $controlCr;
}
}
[class*='expand-button'] {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: auto;
width: 100%;
padding: $interiorMarginSm 1px;
font-size: 11px;
[class*='label'] {
text-orientation: mixed;
text-transform: uppercase;
writing-mode: horizontal-tb;
}
}
}
&[class*='--horizontal'] {
> .l-pane__handle {
cursor: col-resize;
@@ -256,7 +289,7 @@
}
/************************** Horizontal Splitter After */
// Example: Tree pane
// Example: Tree pane and Recent Objects
&[class*='-after'] {
margin-right: nth($shellPanePad, 2);
padding-right: nth($shellPanePad, 2);
@@ -292,7 +325,7 @@
}
/************************** Vertical Splitter Before */
// Pane collapses downward. Used by Elements pool in Inspector
// Pane collapses downward. Used by Recent Objects in Tree
&[class*='-before'] {
$m: $interiorMarginLg;
margin-top: $m;
@@ -303,7 +336,7 @@
}
.l-pane__collapse-button:before {
content: $glyph-icon-arrow-down;
content: $glyph-icon-line-horz;
}
&.l-pane--collapsed {

View File

@@ -27,6 +27,11 @@
aria-label="Search Results Dropdown"
class="c-gsearch__dropdown"
>
<button
aria-label="Close"
class="c-gsearch__results-close c-click-icon c-overlay__close-button icon-x"
@click="selectedResult"
></button>
<div class="c-gsearch__results" :class="{ 'search-finished': !searchLoading }">
<div
v-if="objectResults?.length"

View File

@@ -28,6 +28,19 @@
background: rgba($colorHeadFg, 0.2);
box-shadow: none;
flex: 1 1 auto;
body.mobile & {
@include phonePortrait() {
// This logic of expanding the search input upon click only happens in mobile portrait mode
background: $colorHeadBg;
width: 15%;
&:hover {
// When clicked, expand the search bar
background-color: rgba($colorHeadFg, 0.2);
width: 50vw;
transition: width 120ms;
}
}
}
}
}
@@ -40,6 +53,17 @@
max-height: 500px;
top: $formInputH;
z-index: 60;
body.mobile & {
// Makes search in mobile look less like an overlay, and more fullscreen.
position: absolute;
top: 25px;
left: -12px;
height: calc(100vh - 22px);
min-width: 100vw;
max-height: none;
border-radius: 0px;
box-shadow: none;
}
}
&__results,
@@ -55,6 +79,10 @@
> * + * {
margin-top: $interiorMarginLg;
}
body.mobile & {
// Add a margin to results so we have room for the close button
width: 93%;
}
}
&__results-section {
@@ -65,6 +93,7 @@
&__results-section-title {
@include propertiesHeader();
width: 95%;
}
&__result-pane-msg {
@@ -72,6 +101,22 @@
margin-top: $interiorMargin;
}
}
&__results-close {
// Close button that appears for mobile only
display: none;
body.mobile & {
display: block;
}
}
body.mobile & {
width: 50vw;
@include phonePortrait() {
// This logic only appears for a mobile portrait mode
width: 100%;
}
}
}
.c-gsearch-result {
@@ -113,9 +158,9 @@
}
.c-location {
//color: $colorBodyFg;
//font-size: 0.9em;
//opacity: 0.8;
color: $colorBodyFg;
font-size: 0.9em;
opacity: 0.8;
}
}

View File

@@ -22,6 +22,7 @@
import { isProxy, toRaw } from 'vue';
import { isIdentifier } from '@/api/objects/object-utils';
import StalenessUtils from '@/utils/staleness';
export default {
@@ -40,7 +41,9 @@ export default {
},
methods: {
getSubscriptionId(domainObject) {
return this.openmct?.objects.makeKeyString(domainObject.identifier);
// Only extract the identifier if it is not already an identifier
const identifier = isIdentifier(domainObject) ? domainObject : domainObject.identifier;
return this.openmct?.objects.makeKeyString(identifier);
},
setupClockChangedEvent(callback) {
this.setupClockChanged = true;
@@ -97,6 +100,7 @@ export default {
if (isProxy(domainObject)) {
domainObject = toRaw(domainObject);
}
const id = this.getSubscriptionId(domainObject);
if (!this.stalenessSubscription[id]) {
return;