mirror of
https://github.com/nasa/openmct.git
synced 2024-12-24 07:16:39 +00:00
Merge branch 'master' into 7475-investigate-couch-version-managment-scripts
This commit is contained in:
commit
d9ffb2dccb
@ -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:
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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](../docs/src/process/release.md) that will require a special callout in the release notes? For example, will this break compatibility with existing APIs or projects that consume these plugins?
|
||||
|
||||
### Author Checklist
|
||||
|
||||
|
5
.github/release.yml
vendored
5
.github/release.yml
vendored
@ -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:
|
||||
- '*'
|
||||
- "*"
|
||||
|
16
.github/workflows/e2e-flakefinder.yml
vendored
16
.github/workflows/e2e-flakefinder.yml
vendored
@ -1,15 +1,15 @@
|
||||
name: 'pr:e2e:flakefinder'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
# push:
|
||||
# branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
# pull_request:
|
||||
# types:
|
||||
# - labeled
|
||||
# - opened
|
||||
# schedule:
|
||||
# - cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
e2e-flakefinder:
|
||||
|
30
docs/src/process/release.md
Normal file
30
docs/src/process/release.md
Normal 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.
|
@ -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`.|
|
||||
@ -238,6 +238,7 @@ Current list of test tags:
|
||||
|`@unstable` | A new test or test which is known to be flaky.|
|
||||
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
|
||||
|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|
|
||||
|`@clock` | A test which modifies the clock. These have expanded out of the visual tests and into the functional tests.
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
@ -329,9 +330,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:**
|
||||
|
||||
@ -441,6 +448,7 @@ By adhering to this principle, we can create tests that are both robust and refl
|
||||
- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
|
||||
- Use Open MCT's fixed-time mode unless explicitly testing realtime clock
|
||||
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
|
||||
- Avoid creating objects with a time component like timers and clocks.
|
||||
|
||||
5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:
|
||||
- `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
|
||||
|
@ -21,10 +21,12 @@
|
||||
*****************************************************************************/
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { expect } from '../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultManagementWithExample(page) {
|
||||
export async function navigateToFaultManagementWithExample(page) {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./addInitExampleFaultProvider.js', import.meta.url))
|
||||
});
|
||||
@ -35,7 +37,7 @@ async function navigateToFaultManagementWithExample(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultManagementWithStaticExample(page) {
|
||||
export async function navigateToFaultManagementWithStaticExample(page) {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./addInitExampleFaultProviderStatic.js', import.meta.url))
|
||||
});
|
||||
@ -46,7 +48,7 @@ async function navigateToFaultManagementWithStaticExample(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultManagementWithoutExample(page) {
|
||||
export async function navigateToFaultManagementWithoutExample(page) {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./addInitFaultManagementPlugin.js', import.meta.url))
|
||||
});
|
||||
@ -57,7 +59,7 @@ async function navigateToFaultManagementWithoutExample(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultItemInTree(page) {
|
||||
export async function navigateToFaultItemInTree(page) {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const faultManagementTreeItem = page
|
||||
@ -75,88 +77,95 @@ async function navigateToFaultItemInTree(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function acknowledgeFault(page, rowNumber) {
|
||||
export async function acknowledgeFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
await page.locator('.c-menu >> text="Acknowledge"').click();
|
||||
// Click [aria-label="Save"]
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
await page.getByLabel('Acknowledge', { exact: true }).click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function shelveMultipleFaults(page, ...nums) {
|
||||
export async function shelveMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
return selectFaultItem(page, num);
|
||||
});
|
||||
await Promise.all(selectRows);
|
||||
|
||||
await page.locator('button:has-text("Shelve")').click();
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
await page.getByLabel('Shelve selected faults').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function acknowledgeMultipleFaults(page, ...nums) {
|
||||
export async function acknowledgeMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
return selectFaultItem(page, num);
|
||||
});
|
||||
await Promise.all(selectRows);
|
||||
|
||||
await page.locator('button:has-text("Acknowledge")').click();
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function shelveFault(page, rowNumber) {
|
||||
export async function shelveFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
await page.locator('.c-menu >> text="Shelve"').click();
|
||||
// Click [aria-label="Save"]
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function changeViewTo(page, view) {
|
||||
export async function changeViewTo(page, view) {
|
||||
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function sortFaultsBy(page, sort) {
|
||||
export async function sortFaultsBy(page, sort) {
|
||||
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enterSearchTerm(page, term) {
|
||||
export async function enterSearchTerm(page, term) {
|
||||
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function clearSearch(page) {
|
||||
export async function clearSearch(page) {
|
||||
await enterSearchTerm(page, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function selectFaultItem(page, rowNumber) {
|
||||
await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check();
|
||||
export async function selectFaultItem(page, rowNumber) {
|
||||
await page
|
||||
.getByLabel('Select fault')
|
||||
.nth(rowNumber - 1)
|
||||
.check({
|
||||
// Need force here because checkbox state is changed by an event emitted by the checkbox
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await expect(page.getByLabel('Select fault').nth(rowNumber - 1)).toBeChecked();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getHighestSeverity(page) {
|
||||
export async function getHighestSeverity(page) {
|
||||
const criticalCount = await page.locator('[title=CRITICAL]').count();
|
||||
const warningCount = await page.locator('[title=WARNING]').count();
|
||||
|
||||
@ -172,7 +181,7 @@ async function getHighestSeverity(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getLowestSeverity(page) {
|
||||
export async function getLowestSeverity(page) {
|
||||
const warningCount = await page.locator('[title=WARNING]').count();
|
||||
const watchCount = await page.locator('[title=WATCH]').count();
|
||||
|
||||
@ -188,7 +197,7 @@ async function getLowestSeverity(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultResultCount(page) {
|
||||
export async function getFaultResultCount(page) {
|
||||
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
|
||||
|
||||
return count;
|
||||
@ -197,7 +206,7 @@ async function getFaultResultCount(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
function getFault(page, rowNumber) {
|
||||
export function getFault(page, rowNumber) {
|
||||
const fault = page.locator(
|
||||
`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`
|
||||
);
|
||||
@ -208,7 +217,7 @@ function getFault(page, rowNumber) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
function getFaultByName(page, name) {
|
||||
export function getFaultByName(page, name) {
|
||||
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
|
||||
|
||||
return fault;
|
||||
@ -217,7 +226,7 @@ function getFaultByName(page, name) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultName(page, rowNumber) {
|
||||
export async function getFaultName(page, rowNumber) {
|
||||
const faultName = await page
|
||||
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
|
||||
.textContent();
|
||||
@ -228,7 +237,7 @@ async function getFaultName(page, rowNumber) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultSeverity(page, rowNumber) {
|
||||
export async function getFaultSeverity(page, rowNumber) {
|
||||
const faultSeverity = await page
|
||||
.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`)
|
||||
.getAttribute('title');
|
||||
@ -239,7 +248,7 @@ async function getFaultSeverity(page, rowNumber) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultNamespace(page, rowNumber) {
|
||||
export async function getFaultNamespace(page, rowNumber) {
|
||||
const faultNamespace = await page
|
||||
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
|
||||
.textContent();
|
||||
@ -250,7 +259,7 @@ async function getFaultNamespace(page, rowNumber) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultTriggerTime(page, rowNumber) {
|
||||
export async function getFaultTriggerTime(page, rowNumber) {
|
||||
const faultTriggerTime = await page
|
||||
.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`)
|
||||
.textContent();
|
||||
@ -261,35 +270,10 @@ async function getFaultTriggerTime(page, rowNumber) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function openFaultRowMenu(page, rowNumber) {
|
||||
export async function openFaultRowMenu(page, rowNumber) {
|
||||
// select
|
||||
await page
|
||||
.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`)
|
||||
.getByLabel('Disposition actions')
|
||||
.nth(rowNumber - 1)
|
||||
.click();
|
||||
}
|
||||
|
||||
export {
|
||||
acknowledgeFault,
|
||||
acknowledgeMultipleFaults,
|
||||
changeViewTo,
|
||||
clearSearch,
|
||||
enterSearchTerm,
|
||||
getFault,
|
||||
getFaultByName,
|
||||
getFaultName,
|
||||
getFaultNamespace,
|
||||
getFaultResultCount,
|
||||
getFaultSeverity,
|
||||
getFaultTriggerTime,
|
||||
getHighestSeverity,
|
||||
getLowestSeverity,
|
||||
navigateToFaultItemInTree,
|
||||
navigateToFaultManagementWithExample,
|
||||
navigateToFaultManagementWithoutExample,
|
||||
navigateToFaultManagementWithStaticExample,
|
||||
openFaultRowMenu,
|
||||
selectFaultItem,
|
||||
shelveFault,
|
||||
shelveMultipleFaults,
|
||||
sortFaultsBy
|
||||
};
|
||||
|
@ -20,6 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../appActions.js';
|
||||
import { expect } from '../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
@ -142,6 +143,18 @@ export function getLatestEndTime(planJson) {
|
||||
return Math.max(...activities.map((activity) => activity.end));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} planJson
|
||||
* @returns {object}
|
||||
*/
|
||||
export function getFirstActivity(planJson) {
|
||||
const groups = Object.keys(planJson);
|
||||
const firstGroupKey = groups[0];
|
||||
const firstGroupItems = planJson[firstGroupKey];
|
||||
return firstGroupItems[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the Open MCT API to set the status of a plan to 'draft'.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
@ -172,3 +185,55 @@ export async function addPlanGetInterceptor(page) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Plan from JSON and add it to a Timelist and Navigate to the Plan view
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function createTimelistWithPlanAndSetActivityInProgress(page, planJson) {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const timelist = await createDomainObjectWithDefaults(page, {
|
||||
name: 'Time List',
|
||||
type: 'Time List'
|
||||
});
|
||||
|
||||
await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: planJson,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
|
||||
// Ensure that all activities are shown in the expanded view
|
||||
const groups = Object.keys(planJson);
|
||||
const firstGroupKey = groups[0];
|
||||
const firstGroupItems = planJson[firstGroupKey];
|
||||
const firstActivityForPlan = firstGroupItems[0];
|
||||
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
|
||||
const startBound = firstActivityForPlan.start;
|
||||
const endBound = lastActivity.end;
|
||||
|
||||
// Switch to fixed time mode with all plan events within the bounds
|
||||
await page.goto(
|
||||
`${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
|
||||
);
|
||||
|
||||
// Change the object to edit mode
|
||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||
|
||||
// Find the display properties section in the inspector
|
||||
await page.getByRole('tab', { name: 'View Properties' }).click();
|
||||
// Switch to expanded view and save the setting
|
||||
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
|
||||
|
||||
// Click on the "Save" button
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
const anActivity = page.getByRole('row').nth(0);
|
||||
|
||||
// Set the activity to in progress
|
||||
await anActivity.click();
|
||||
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||
await page.getByLabel('Activity Status', { exact: true }).selectOption({ label: 'In progress' });
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -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: [
|
||||
|
69
e2e/playwright-mobile.config.js
Normal file
69
e2e/playwright-mobile.config.js
Normal file
@ -0,0 +1,69 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
import { devices } from '@playwright/test';
|
||||
const MAX_FAILURES = 5;
|
||||
|
||||
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: 1, //Limit to 1 due to resource constraints similar to https://github.com/percy/cli/discussions/1067
|
||||
|
||||
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;
|
@ -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: [
|
||||
|
@ -39,7 +39,7 @@ import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
const overlayPlotName = 'Overlay Plot with Telemetry Object';
|
||||
|
||||
test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
|
||||
test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: MISSION_TIME,
|
||||
|
@ -22,29 +22,13 @@
|
||||
import fs from 'fs';
|
||||
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
|
||||
import { getEarliestStartTime } from '../../../helper/planningUtils';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const examplePlanSmall3 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
|
||||
)
|
||||
);
|
||||
const examplePlanSmall1 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
|
||||
)
|
||||
);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const START_TIME_COLUMN = 0;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const END_TIME_COLUMN = 1;
|
||||
const TIME_TO_FROM_COLUMN = 2;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const ACTIVITY_COLUMN = 3;
|
||||
const HEADER_ROW = 0;
|
||||
const NUM_COLUMNS = 5;
|
||||
|
||||
test.describe('Time List', () => {
|
||||
test("Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties", async ({
|
||||
page
|
||||
@ -161,7 +145,7 @@ test("View a timelist in expanded view, verify all the activities are displayed
|
||||
await expect(eventCount).toEqual(firstGroupItems.length);
|
||||
});
|
||||
|
||||
await test.step('Shows activity properties when a row is selected', async () => {
|
||||
await test.step('Shows activity properties when a row is selected in the expanded view', async () => {
|
||||
await page.getByRole('row').nth(2).click();
|
||||
|
||||
// Find the activity state section in the inspector
|
||||
@ -171,167 +155,10 @@ test("View a timelist in expanded view, verify all the activities are displayed
|
||||
'Not started'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The regular expression used to parse the countdown string.
|
||||
* Some examples of valid Countdown strings:
|
||||
* ```
|
||||
* '35D 02:03:04'
|
||||
* '-1D 01:02:03'
|
||||
* '01:02:03'
|
||||
* '-05:06:07'
|
||||
* ```
|
||||
*/
|
||||
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;
|
||||
|
||||
/**
|
||||
* @typedef {Object} CountdownOrUpObject
|
||||
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
|
||||
* @property {string} days - The number of days in the countdown (undefined if there are no days).
|
||||
* @property {string} hours - The number of hours in the countdown.
|
||||
* @property {string} minutes - The number of minutes in the countdown.
|
||||
* @property {string} seconds - The number of seconds in the countdown.
|
||||
* @property {string} toString - The countdown string.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Object representing the indices of the capture groups in a countdown regex match.
|
||||
*
|
||||
* @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}
|
||||
* @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).
|
||||
* @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).
|
||||
* @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).
|
||||
* @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).
|
||||
* @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).
|
||||
*/
|
||||
const COUNTDOWN = Object.freeze({
|
||||
SIGN: 1,
|
||||
DAYS: 2,
|
||||
HOURS: 3,
|
||||
MINUTES: 4,
|
||||
SECONDS: 5
|
||||
});
|
||||
|
||||
test.describe('Time List with controlled clock', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: getEarliestStartTime(examplePlanSmall3),
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test('Time List shows current events and counts down correctly in real-time mode', async ({
|
||||
page
|
||||
}) => {
|
||||
await test.step('Create a Time List, add a Plan to it, and switch to real-time mode', async () => {
|
||||
// Create Time List
|
||||
const timelist = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time List'
|
||||
});
|
||||
|
||||
// Create a Plan with events that count down and up.
|
||||
// Add it as a child to the Time List.
|
||||
await createPlanFromJSON(page, {
|
||||
json: examplePlanSmall3,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
|
||||
// Navigate to the Time List in real-time mode
|
||||
await page.goto(
|
||||
`${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid`
|
||||
);
|
||||
});
|
||||
|
||||
const countUpCells = [
|
||||
getCellByIndex(page, 1, TIME_TO_FROM_COLUMN),
|
||||
getCellByIndex(page, 2, TIME_TO_FROM_COLUMN)
|
||||
];
|
||||
const countdownCells = [
|
||||
getCellByIndex(page, 3, TIME_TO_FROM_COLUMN),
|
||||
getCellByIndex(page, 4, TIME_TO_FROM_COLUMN)
|
||||
];
|
||||
|
||||
// Verify that the countdown cells are counting down
|
||||
for (let i = 0; i < countdownCells.length; i++) {
|
||||
await test.step(`Countdown cell ${i + 1} counts down`, async () => {
|
||||
const countdownCell = countdownCells[i];
|
||||
// Get the initial countdown timestamp object
|
||||
const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
|
||||
// should not have a '-' sign
|
||||
await expect(countdownCell).not.toHaveText('-');
|
||||
// Wait until it changes
|
||||
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
|
||||
// Get the new countdown timestamp object
|
||||
const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
|
||||
// Verify that the new countdown timestamp object is less than the old one
|
||||
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
|
||||
});
|
||||
}
|
||||
|
||||
// Verify that the count-up cells are counting up
|
||||
for (let i = 0; i < countUpCells.length; i++) {
|
||||
await test.step(`Count-up cell ${i + 1} counts up`, async () => {
|
||||
const countUpCell = countUpCells[i];
|
||||
// Get the initial count-up timestamp object
|
||||
const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
|
||||
// should not have a '+' sign
|
||||
await expect(countUpCell).not.toHaveText('+');
|
||||
// Wait until it changes
|
||||
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
|
||||
// Get the new count-up timestamp object
|
||||
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
|
||||
// Verify that the new count-up timestamp object is greater than the old one
|
||||
expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
|
||||
});
|
||||
}
|
||||
await test.step("Verify absence of progress indication for an activity that's not in progress", async () => {
|
||||
// When an activity is not in progress, the progress pie is not visible
|
||||
const hidden = await page.getByRole('row').locator('path').nth(1).isHidden();
|
||||
await expect(hidden).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the cell at the given row and column indices.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowIndex
|
||||
* @param {number} columnIndex
|
||||
* @returns {import('@playwright/test').Locator} cell
|
||||
*/
|
||||
function getCellByIndex(page, rowIndex, columnIndex) {
|
||||
return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the innerText of the cell at the given row and column indices.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowIndex
|
||||
* @param {number} columnIndex
|
||||
* @returns {Promise<string>} text
|
||||
*/
|
||||
async function getCellTextByIndex(page, rowIndex, columnIndex) {
|
||||
const text = await getCellByIndex(page, rowIndex, columnIndex).innerText();
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
|
||||
* regex, and return an object representing the countdown.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowIndex the row index
|
||||
* @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
|
||||
*/
|
||||
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
|
||||
const timeToFrom = await getCellTextByIndex(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN);
|
||||
|
||||
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
|
||||
const match = timeToFrom.match(COUNTDOWN_REGEXP);
|
||||
|
||||
return {
|
||||
sign: match[COUNTDOWN.SIGN],
|
||||
days: match[COUNTDOWN.DAYS],
|
||||
hours: match[COUNTDOWN.HOURS],
|
||||
minutes: match[COUNTDOWN.MINUTES],
|
||||
seconds: match[COUNTDOWN.SECONDS],
|
||||
toString: () => timeToFrom
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,290 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Collection of Time List tests set to run with browser clock manipulate made possible with the
|
||||
clockOptions plugin fixture.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
|
||||
import {
|
||||
createTimelistWithPlanAndSetActivityInProgress,
|
||||
getEarliestStartTime,
|
||||
getFirstActivity
|
||||
} from '../../../helper/planningUtils';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const examplePlanSmall3 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
|
||||
)
|
||||
);
|
||||
|
||||
const examplePlanSmall1 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
|
||||
)
|
||||
);
|
||||
|
||||
const TIME_TO_FROM_COLUMN = 2;
|
||||
const HEADER_ROW = 0;
|
||||
const NUM_COLUMNS = 5;
|
||||
const FULL_CIRCLE_PATH =
|
||||
'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';
|
||||
|
||||
/**
|
||||
* The regular expression used to parse the countdown string.
|
||||
* Some examples of valid Countdown strings:
|
||||
* ```
|
||||
* '35D 02:03:04'
|
||||
* '-1D 01:02:03'
|
||||
* '01:02:03'
|
||||
* '-05:06:07'
|
||||
* ```
|
||||
*/
|
||||
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;
|
||||
|
||||
/**
|
||||
* @typedef {Object} CountdownOrUpObject
|
||||
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
|
||||
* @property {string} days - The number of days in the countdown (undefined if there are no days).
|
||||
* @property {string} hours - The number of hours in the countdown.
|
||||
* @property {string} minutes - The number of minutes in the countdown.
|
||||
* @property {string} seconds - The number of seconds in the countdown.
|
||||
* @property {string} toString - The countdown string.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Object representing the indices of the capture groups in a countdown regex match.
|
||||
*
|
||||
* @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}
|
||||
* @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).
|
||||
* @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).
|
||||
* @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).
|
||||
* @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).
|
||||
* @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).
|
||||
*/
|
||||
const COUNTDOWN = Object.freeze({
|
||||
SIGN: 1,
|
||||
DAYS: 2,
|
||||
HOURS: 3,
|
||||
MINUTES: 4,
|
||||
SECONDS: 5
|
||||
});
|
||||
|
||||
test.describe('Time List with controlled clock @clock', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: getEarliestStartTime(examplePlanSmall3),
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create Time List
|
||||
const timelist = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time List'
|
||||
});
|
||||
|
||||
// Create a Plan with events that count down and up.
|
||||
// Add it as a child to the Time List.
|
||||
await createPlanFromJSON(page, {
|
||||
json: examplePlanSmall3,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
|
||||
// Navigate to the Time List in real-time mode
|
||||
await page.goto(
|
||||
`${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid`
|
||||
);
|
||||
|
||||
//Expand the viewport to show the entire time list
|
||||
await page.getByLabel('Collapse Inspect Pane').click();
|
||||
await page.getByLabel('Collapse Browse Pane').click();
|
||||
});
|
||||
test('Time List shows current events and counts down correctly in real-time mode', async ({
|
||||
page
|
||||
}) => {
|
||||
const countUpCells = [
|
||||
getTimeListCellByIndex(page, 1, TIME_TO_FROM_COLUMN),
|
||||
getTimeListCellByIndex(page, 2, TIME_TO_FROM_COLUMN)
|
||||
];
|
||||
const countdownCells = [
|
||||
getTimeListCellByIndex(page, 3, TIME_TO_FROM_COLUMN),
|
||||
getTimeListCellByIndex(page, 4, TIME_TO_FROM_COLUMN)
|
||||
];
|
||||
|
||||
// Verify that the countdown cells are counting down
|
||||
for (let i = 0; i < countdownCells.length; i++) {
|
||||
await test.step(`Countdown cell ${i + 1} counts down`, async () => {
|
||||
const countdownCell = countdownCells[i];
|
||||
// Get the initial countdown timestamp object
|
||||
const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
|
||||
// should not have a '-' sign
|
||||
await expect(countdownCell).not.toHaveText('-');
|
||||
// Wait until it changes
|
||||
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
|
||||
// Get the new countdown timestamp object
|
||||
const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
|
||||
// Verify that the new countdown timestamp object is less than the old one
|
||||
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
|
||||
});
|
||||
}
|
||||
|
||||
// Verify that the count-up cells are counting up
|
||||
for (let i = 0; i < countUpCells.length; i++) {
|
||||
await test.step(`Count-up cell ${i + 1} counts up`, async () => {
|
||||
const countUpCell = countUpCells[i];
|
||||
// Get the initial count-up timestamp object
|
||||
const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
|
||||
// should not have a '+' sign
|
||||
await expect(countUpCell).not.toHaveText('+');
|
||||
// Wait until it changes
|
||||
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
|
||||
// Get the new count-up timestamp object
|
||||
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
|
||||
// Verify that the new count-up timestamp object is greater than the old one
|
||||
expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Activity progress when activity is in the future @clock', () => {
|
||||
const firstActivity = getFirstActivity(examplePlanSmall1);
|
||||
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: firstActivity.start - 1,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
|
||||
});
|
||||
|
||||
test('progress pie is empty', async ({ page }) => {
|
||||
const anActivity = page.getByRole('row').nth(0);
|
||||
// Progress pie shows no progress when now is less than the start time
|
||||
await expect(anActivity.getByLabel('Activity in progress').locator('path')).not.toHaveAttribute(
|
||||
'd'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Activity progress when now is between start and end of the activity @clock', () => {
|
||||
const firstActivity = getFirstActivity(examplePlanSmall1);
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
|
||||
});
|
||||
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: firstActivity.start + 50000,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test('progress pie is partially filled', async ({ page }) => {
|
||||
const anActivity = page.getByRole('row').nth(0);
|
||||
const pathElement = anActivity.getByLabel('Activity in progress').locator('path');
|
||||
// Progress pie shows progress when now is greater than the start time
|
||||
await expect(pathElement).toHaveAttribute('d');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Activity progress when now is after end of the activity @clock', () => {
|
||||
const firstActivity = getFirstActivity(examplePlanSmall1);
|
||||
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: firstActivity.end + 10000,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
|
||||
});
|
||||
|
||||
test('progress pie is full', async ({ page }) => {
|
||||
const anActivity = page.getByRole('row').nth(0);
|
||||
// Progress pie is completely full and doesn't update if now is greater than the end time
|
||||
await expect(anActivity.getByLabel('Activity in progress').locator('path')).toHaveAttribute(
|
||||
'd',
|
||||
FULL_CIRCLE_PATH
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the cell at the given row and column indices.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowIndex
|
||||
* @param {number} columnIndex
|
||||
* @returns {import('@playwright/test').Locator} cell
|
||||
*/
|
||||
function getTimeListCellByIndex(page, rowIndex, columnIndex) {
|
||||
return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the innerText of the cell at the given row and column indices.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowIndex
|
||||
* @param {number} columnIndex
|
||||
* @returns {Promise<string>} text
|
||||
*/
|
||||
async function getTimeListCellTextByIndex(page, rowIndex, columnIndex) {
|
||||
const text = await getTimeListCellByIndex(page, rowIndex, columnIndex).innerText();
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
|
||||
* regex, and return an object representing the countdown.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowIndex the row index
|
||||
* @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
|
||||
*/
|
||||
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
|
||||
const timeToFrom = await getTimeListCellTextByIndex(
|
||||
page,
|
||||
HEADER_ROW + rowIndex,
|
||||
TIME_TO_FROM_COLUMN
|
||||
);
|
||||
|
||||
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
|
||||
const match = timeToFrom.match(COUNTDOWN_REGEXP);
|
||||
|
||||
return {
|
||||
sign: match[COUNTDOWN.SIGN],
|
||||
days: match[COUNTDOWN.DAYS],
|
||||
hours: match[COUNTDOWN.HOURS],
|
||||
minutes: match[COUNTDOWN.MINUTES],
|
||||
seconds: match[COUNTDOWN.SECONDS],
|
||||
toString: () => timeToFrom
|
||||
};
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -20,25 +20,46 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import * as utils from '../../../../helper/faultUtils.js';
|
||||
import {
|
||||
acknowledgeFault,
|
||||
acknowledgeMultipleFaults,
|
||||
changeViewTo,
|
||||
clearSearch,
|
||||
enterSearchTerm,
|
||||
getFault,
|
||||
getFaultByName,
|
||||
getFaultName,
|
||||
getFaultNamespace,
|
||||
getFaultResultCount,
|
||||
getFaultSeverity,
|
||||
getFaultTriggerTime,
|
||||
getHighestSeverity,
|
||||
getLowestSeverity,
|
||||
navigateToFaultManagementWithExample,
|
||||
navigateToFaultManagementWithoutExample,
|
||||
selectFaultItem,
|
||||
shelveFault,
|
||||
shelveMultipleFaults,
|
||||
sortFaultsBy
|
||||
} from '../../../../helper/faultUtils.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('The Fault Management Plugin using example faults', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await utils.navigateToFaultManagementWithExample(page);
|
||||
await navigateToFaultManagementWithExample(page);
|
||||
});
|
||||
|
||||
test('Shows a criticality icon for every fault @unstable', async ({ page }) => {
|
||||
test('Shows a criticality icon for every fault', async ({ page }) => {
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count();
|
||||
|
||||
expect.soft(faultCount).toEqual(criticalityIconCount);
|
||||
expect(faultCount).toEqual(criticalityIconCount);
|
||||
});
|
||||
|
||||
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({
|
||||
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({
|
||||
page
|
||||
}) => {
|
||||
await utils.selectFaultItem(page, 1);
|
||||
await selectFaultItem(page, 1);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
const selectedFaultName = await page
|
||||
@ -48,22 +69,22 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`)
|
||||
.count();
|
||||
|
||||
await expect
|
||||
.soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first())
|
||||
.toHaveClass(/is-selected/);
|
||||
expect.soft(inspectorFaultNameCount).toEqual(1);
|
||||
await expect(
|
||||
page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()
|
||||
).toHaveClass(/is-selected/);
|
||||
expect(inspectorFaultNameCount).toEqual(1);
|
||||
});
|
||||
|
||||
test('When selecting multiple faults, no specific fault information is shown in the inspector @unstable', async ({
|
||||
test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({
|
||||
page
|
||||
}) => {
|
||||
await utils.selectFaultItem(page, 1);
|
||||
await utils.selectFaultItem(page, 2);
|
||||
await selectFaultItem(page, 1);
|
||||
await selectFaultItem(page, 2);
|
||||
|
||||
const selectedRows = page.locator(
|
||||
'.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname'
|
||||
);
|
||||
expect.soft(await selectedRows.count()).toEqual(2);
|
||||
expect(await selectedRows.count()).toEqual(2);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
|
||||
@ -75,180 +96,180 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
|
||||
.count();
|
||||
|
||||
expect.soft(firstNameInInspectorCount).toEqual(0);
|
||||
expect.soft(secondNameInInspectorCount).toEqual(0);
|
||||
expect(firstNameInInspectorCount).toEqual(0);
|
||||
expect(secondNameInInspectorCount).toEqual(0);
|
||||
});
|
||||
|
||||
test('Allows you to shelve a fault @unstable', async ({ page }) => {
|
||||
const shelvedFaultName = await utils.getFaultName(page, 2);
|
||||
const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName);
|
||||
test('Allows you to shelve a fault', async ({ page }) => {
|
||||
const shelvedFaultName = await getFaultName(page, 2);
|
||||
const beforeShelvedFault = getFaultByName(page, shelvedFaultName);
|
||||
|
||||
expect.soft(await beforeShelvedFault.count()).toBe(1);
|
||||
await expect(beforeShelvedFault).toHaveCount(1);
|
||||
|
||||
await utils.shelveFault(page, 2);
|
||||
await shelveFault(page, 2);
|
||||
|
||||
// check it is removed from standard view
|
||||
const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName);
|
||||
expect.soft(await afterShelvedFault.count()).toBe(0);
|
||||
const afterShelvedFault = getFaultByName(page, shelvedFaultName);
|
||||
expect(await afterShelvedFault.count()).toBe(0);
|
||||
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
await changeViewTo(page, 'shelved');
|
||||
|
||||
const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName);
|
||||
const shelvedViewFault = getFaultByName(page, shelvedFaultName);
|
||||
|
||||
expect.soft(await shelvedViewFault.count()).toBe(1);
|
||||
expect(await shelvedViewFault.count()).toBe(1);
|
||||
});
|
||||
|
||||
test('Allows you to acknowledge a fault @unstable', async ({ page }) => {
|
||||
const acknowledgedFaultName = await utils.getFaultName(page, 3);
|
||||
test('Allows you to acknowledge a fault', async ({ page }) => {
|
||||
const acknowledgedFaultName = await getFaultName(page, 3);
|
||||
|
||||
await utils.acknowledgeFault(page, 3);
|
||||
await acknowledgeFault(page, 3);
|
||||
|
||||
const fault = utils.getFault(page, 3);
|
||||
await expect.soft(fault).toHaveClass(/is-acknowledged/);
|
||||
const fault = getFault(page, 3);
|
||||
await expect(fault).toHaveClass(/is-acknowledged/);
|
||||
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
await changeViewTo(page, 'acknowledged');
|
||||
|
||||
const acknowledgedViewFaultName = await utils.getFaultName(page, 1);
|
||||
expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
|
||||
const acknowledgedViewFaultName = await getFaultName(page, 1);
|
||||
expect(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
|
||||
});
|
||||
|
||||
test('Allows you to shelve multiple faults @unstable', async ({ page }) => {
|
||||
const shelvedFaultNameOne = await utils.getFaultName(page, 1);
|
||||
const shelvedFaultNameFour = await utils.getFaultName(page, 4);
|
||||
test('Allows you to shelve multiple faults', async ({ page }) => {
|
||||
const shelvedFaultNameOne = await getFaultName(page, 1);
|
||||
const shelvedFaultNameFour = await getFaultName(page, 4);
|
||||
|
||||
const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||
const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||
const beforeShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
|
||||
const beforeShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
|
||||
|
||||
expect.soft(await beforeShelvedFaultOne.count()).toBe(1);
|
||||
expect.soft(await beforeShelvedFaultFour.count()).toBe(1);
|
||||
await expect(beforeShelvedFaultOne).toHaveCount(1);
|
||||
await expect(beforeShelvedFaultFour).toHaveCount(1);
|
||||
|
||||
await utils.shelveMultipleFaults(page, 1, 4);
|
||||
await shelveMultipleFaults(page, 1, 4);
|
||||
|
||||
// check it is removed from standard view
|
||||
const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||
const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||
expect.soft(await afterShelvedFaultOne.count()).toBe(0);
|
||||
expect.soft(await afterShelvedFaultFour.count()).toBe(0);
|
||||
const afterShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
|
||||
const afterShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
|
||||
await expect(afterShelvedFaultOne).toHaveCount(0);
|
||||
await expect(afterShelvedFaultFour).toHaveCount(0);
|
||||
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
await changeViewTo(page, 'shelved');
|
||||
|
||||
const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||
const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||
const shelvedViewFaultOne = getFaultByName(page, shelvedFaultNameOne);
|
||||
const shelvedViewFaultFour = getFaultByName(page, shelvedFaultNameFour);
|
||||
|
||||
expect.soft(await shelvedViewFaultOne.count()).toBe(1);
|
||||
expect.soft(await shelvedViewFaultFour.count()).toBe(1);
|
||||
await expect(shelvedViewFaultOne).toHaveCount(1);
|
||||
await expect(shelvedViewFaultFour).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('Allows you to acknowledge multiple faults @unstable', async ({ page }) => {
|
||||
const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2);
|
||||
const acknowledgedFaultNameFive = await utils.getFaultName(page, 5);
|
||||
test('Allows you to acknowledge multiple faults', async ({ page }) => {
|
||||
const acknowledgedFaultNameTwo = await getFaultName(page, 2);
|
||||
const acknowledgedFaultNameFive = await getFaultName(page, 5);
|
||||
|
||||
await utils.acknowledgeMultipleFaults(page, 2, 5);
|
||||
await acknowledgeMultipleFaults(page, 2, 5);
|
||||
|
||||
const faultTwo = utils.getFault(page, 2);
|
||||
const faultFive = utils.getFault(page, 5);
|
||||
const faultTwo = getFault(page, 2);
|
||||
const faultFive = getFault(page, 5);
|
||||
|
||||
// check they have been acknowledged
|
||||
await expect.soft(faultTwo).toHaveClass(/is-acknowledged/);
|
||||
await expect.soft(faultFive).toHaveClass(/is-acknowledged/);
|
||||
await expect(faultTwo).toHaveClass(/is-acknowledged/);
|
||||
await expect(faultFive).toHaveClass(/is-acknowledged/);
|
||||
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
await changeViewTo(page, 'acknowledged');
|
||||
|
||||
const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo);
|
||||
const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive);
|
||||
const acknowledgedViewFaultTwo = getFaultByName(page, acknowledgedFaultNameTwo);
|
||||
const acknowledgedViewFaultFive = getFaultByName(page, acknowledgedFaultNameFive);
|
||||
|
||||
expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1);
|
||||
expect.soft(await acknowledgedViewFaultFive.count()).toBe(1);
|
||||
await expect(acknowledgedViewFaultTwo).toHaveCount(1);
|
||||
await expect(acknowledgedViewFaultFive).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('Allows you to search faults @unstable', async ({ page }) => {
|
||||
const faultThreeNamespace = await utils.getFaultNamespace(page, 3);
|
||||
const faultTwoName = await utils.getFaultName(page, 2);
|
||||
const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5);
|
||||
test('Allows you to search faults', async ({ page }) => {
|
||||
const faultThreeNamespace = await getFaultNamespace(page, 3);
|
||||
const faultTwoName = await getFaultName(page, 2);
|
||||
const faultFiveTriggerTime = await getFaultTriggerTime(page, 5);
|
||||
|
||||
// should be all faults (5)
|
||||
let faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(5);
|
||||
let faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
|
||||
// search namespace
|
||||
await utils.enterSearchTerm(page, faultThreeNamespace);
|
||||
await enterSearchTerm(page, faultThreeNamespace);
|
||||
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(1);
|
||||
expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
|
||||
|
||||
// all faults
|
||||
await utils.clearSearch(page);
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(5);
|
||||
await clearSearch(page);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
|
||||
// search name
|
||||
await utils.enterSearchTerm(page, faultTwoName);
|
||||
await enterSearchTerm(page, faultTwoName);
|
||||
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(1);
|
||||
expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
expect(await getFaultName(page, 1)).toEqual(faultTwoName);
|
||||
|
||||
// all faults
|
||||
await utils.clearSearch(page);
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(5);
|
||||
await clearSearch(page);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
|
||||
// search triggerTime
|
||||
await utils.enterSearchTerm(page, faultFiveTriggerTime);
|
||||
await enterSearchTerm(page, faultFiveTriggerTime);
|
||||
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(1);
|
||||
expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
|
||||
});
|
||||
|
||||
test('Allows you to sort faults @unstable', async ({ page }) => {
|
||||
const highestSeverity = await utils.getHighestSeverity(page);
|
||||
const lowestSeverity = await utils.getLowestSeverity(page);
|
||||
test('Allows you to sort faults', async ({ page }) => {
|
||||
const highestSeverity = await getHighestSeverity(page);
|
||||
const lowestSeverity = await getLowestSeverity(page);
|
||||
const faultOneName = 'Example Fault 1';
|
||||
const faultFiveName = 'Example Fault 5';
|
||||
let firstFaultName = await utils.getFaultName(page, 1);
|
||||
let firstFaultName = await getFaultName(page, 1);
|
||||
|
||||
expect.soft(firstFaultName).toEqual(faultOneName);
|
||||
expect(firstFaultName).toEqual(faultOneName);
|
||||
|
||||
await utils.sortFaultsBy(page, 'oldest-first');
|
||||
await sortFaultsBy(page, 'oldest-first');
|
||||
|
||||
firstFaultName = await utils.getFaultName(page, 1);
|
||||
expect.soft(firstFaultName).toEqual(faultFiveName);
|
||||
firstFaultName = await getFaultName(page, 1);
|
||||
expect(firstFaultName).toEqual(faultFiveName);
|
||||
|
||||
await utils.sortFaultsBy(page, 'severity');
|
||||
await sortFaultsBy(page, 'severity');
|
||||
|
||||
const sortedHighestSeverity = await utils.getFaultSeverity(page, 1);
|
||||
const sortedLowestSeverity = await utils.getFaultSeverity(page, 5);
|
||||
expect.soft(sortedHighestSeverity).toEqual(highestSeverity);
|
||||
expect.soft(sortedLowestSeverity).toEqual(lowestSeverity);
|
||||
const sortedHighestSeverity = await getFaultSeverity(page, 1);
|
||||
const sortedLowestSeverity = await getFaultSeverity(page, 5);
|
||||
expect(sortedHighestSeverity).toEqual(highestSeverity);
|
||||
expect(sortedLowestSeverity).toEqual(lowestSeverity);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('The Fault Management Plugin without using example faults', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await utils.navigateToFaultManagementWithoutExample(page);
|
||||
await navigateToFaultManagementWithoutExample(page);
|
||||
});
|
||||
|
||||
test('Shows no faults when no faults are provided @unstable', async ({ page }) => {
|
||||
test('Shows no faults when no faults are provided', async ({ page }) => {
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
|
||||
expect.soft(faultCount).toEqual(0);
|
||||
expect(faultCount).toEqual(0);
|
||||
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
await changeViewTo(page, 'acknowledged');
|
||||
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
|
||||
expect.soft(acknowledgedCount).toEqual(0);
|
||||
expect(acknowledgedCount).toEqual(0);
|
||||
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
await changeViewTo(page, 'shelved');
|
||||
const shelvedCount = await page.locator('c-fault-mgmt__list').count();
|
||||
expect.soft(shelvedCount).toEqual(0);
|
||||
expect(shelvedCount).toEqual(0);
|
||||
});
|
||||
|
||||
test('Will return no faults when searching @unstable', async ({ page }) => {
|
||||
await utils.enterSearchTerm(page, 'fault');
|
||||
test('Will return no faults when searching', async ({ page }) => {
|
||||
await enterSearchTerm(page, 'fault');
|
||||
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
|
||||
expect.soft(faultCount).toEqual(0);
|
||||
expect(faultCount).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
@ -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 }) => {
|
||||
|
@ -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,
|
||||
|
@ -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 });
|
||||
|
@ -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',
|
||||
@ -196,13 +189,60 @@ test.describe('Overlay Plot', () => {
|
||||
await assertLimitLinesExistAndAreVisible(page);
|
||||
});
|
||||
|
||||
test('The elements pool supports dragging series into multiple y-axis buckets', async ({
|
||||
page
|
||||
}) => {
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
test('Limit lines adjust when series is resized', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6987'
|
||||
});
|
||||
// Create an Overlay Plot with a default SWG
|
||||
overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
// Assert that no limit lines are shown by default
|
||||
await page.waitForSelector('.js-limit-area', { state: 'attached' });
|
||||
expect(await page.locator('.c-plot-limit-line').count()).toBe(0);
|
||||
|
||||
// Enter edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Expand the "Sine Wave Generator" plot series options and enable limit lines
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.getByRole('checkbox', { name: 'Limit lines' })
|
||||
.check();
|
||||
|
||||
await assertLimitLinesExistAndAreVisible(page);
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
const initialCoords = await assertLimitLinesExistAndAreVisible(page);
|
||||
// Resize the chart container by showing the snapshot pane.
|
||||
await page.getByLabel('Show Snapshots').click();
|
||||
|
||||
const newCoords = await assertLimitLinesExistAndAreVisible(page);
|
||||
// We just need to know that the first limit line redrew somewhere lower than the initial y position.
|
||||
expect(newCoords.y).toBeGreaterThan(initialCoords.y);
|
||||
});
|
||||
|
||||
test('The elements pool supports dragging series into multiple y-axis buckets', async ({
|
||||
page
|
||||
}) => {
|
||||
const swgA = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
@ -292,10 +332,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 +345,53 @@ 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
|
||||
});
|
||||
const swgB = 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();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7530'
|
||||
});
|
||||
await test.step('Verify that the legend is correct after removing a series', async () => {
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
await page.mouse.move(50, 0, {
|
||||
steps: 10
|
||||
});
|
||||
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(1);
|
||||
await expect(page.getByLabel(`Plot Legend Item for ${swgA.name}`)).toBeHidden();
|
||||
await expect(page.getByLabel(`Plot Legend Item for ${swgB.name}`)).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@ -332,4 +409,7 @@ async function assertLimitLinesExistAndAreVisible(page) {
|
||||
for (let i = 0; i < limitLineCount; i++) {
|
||||
await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();
|
||||
}
|
||||
|
||||
const firstLimitLineCoords = await page.locator('.c-plot-limit-line').first().boundingBox();
|
||||
return firstLimitLineCoords;
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -35,7 +35,7 @@ const defaultFrameBorderColor = '#e6b8af'; //default border color
|
||||
const defaultBorderTargetColor = '#acacac';
|
||||
const defaultTextColor = '#acacac'; // default text color
|
||||
const inheritedColor = '#acacac'; // inherited from the body style
|
||||
const pukeGreen = '#6aa84f'; //Ugliest green known to man
|
||||
const pukeGreen = '#6aa84f'; //Ugliest green known to man 🤮
|
||||
const NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value
|
||||
|
||||
test.describe('Flexible Layout styling', () => {
|
||||
@ -411,4 +411,39 @@ test.describe('Flexible Layout styling', () => {
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
test('Styling, and then canceling reverts to previous style', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7233'
|
||||
});
|
||||
|
||||
await page.goto(flexibleLayout.url);
|
||||
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
await setStyles(
|
||||
page,
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('Flexible Layout Column')
|
||||
);
|
||||
await page.getByLabel('Cancel Editing').click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await checkStyles(
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(inheritedColor),
|
||||
page.getByLabel('Flexible Layout Column')
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
await checkStyles(
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(inheritedColor),
|
||||
page.getByLabel('Flexible Layout Column')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,73 @@
|
||||
/*****************************************************************************
|
||||
* 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 preview plugin.
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults, expandEntireTree } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Preview mode', () => {
|
||||
test('all context menu items are available for a telemetry table', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout'
|
||||
});
|
||||
// Create a Telemetry Table
|
||||
const telemetryTable = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
parent: displayLayout.uuid
|
||||
});
|
||||
// Create a Sinewave Generator
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: telemetryTable.uuid
|
||||
});
|
||||
|
||||
await page.goto(displayLayout.url);
|
||||
await page.getByLabel('View menu items').click();
|
||||
await expect(page.getByLabel('Export Marked Rows')).toBeVisible();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Large View' }).click();
|
||||
await page.getByLabel('Overlay').getByLabel('More actions').click();
|
||||
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.getByRole('button', { name: 'Close' }).click();
|
||||
|
||||
await expandEntireTree(page);
|
||||
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const telemetryTableTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(telemetryTable.name)
|
||||
});
|
||||
await telemetryTableTreeItem.locator('a').click();
|
||||
await page.getByLabel('Overlay').getByLabel('More actions').click();
|
||||
await expect(page.getByLabel('Export Table Data')).toBeVisible();
|
||||
await expect(page.getByLabel('Export Marked Rows')).toBeVisible();
|
||||
});
|
||||
});
|
@ -66,7 +66,7 @@ test.describe('Timer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Timer with target date', () => {
|
||||
test.describe('Timer with target date @clock', () => {
|
||||
let timer;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
@ -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() {
|
||||
|
@ -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('./');
|
||||
|
@ -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',
|
||||
|
95
e2e/tests/mobile/smoke.e2e.spec.js
Normal file
95
e2e/tests/mobile/smoke.e2e.spec.js
Normal file
@ -0,0 +1,95 @@
|
||||
/*****************************************************************************
|
||||
* 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.describe('Smoke tests for @mobile', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
|
||||
await page.goto('./');
|
||||
});
|
||||
|
||||
test('Verify that My Items Tree appears @mobile', async ({ page }) => {
|
||||
//My Items to be visible
|
||||
await expect(page.getByRole('treeitem', { name: 'My Items' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Verify that user can search @mobile', async ({ page }) => {
|
||||
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();
|
||||
});
|
||||
|
||||
test('Verify that user can change time conductor @mobile', async ({ page }) => {
|
||||
//Collapse Browse Pane to get more Time Conductor space
|
||||
await page.getByLabel('Collapse Browse Pane').click();
|
||||
//Open Time Conductor and change to Real Time Mode and set offset hour by 1 hour
|
||||
// Disabling line because we're intentionally obscuring the text
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await page.getByLabel('Time Conductor Mode').click({ force: true });
|
||||
await page.getByLabel('Time Conductor Mode Menu').click();
|
||||
await page.getByLabel('Real-Time').click();
|
||||
await page.getByLabel('Start offset hours').fill('01');
|
||||
await page.getByLabel('Submit time offsets').click();
|
||||
await expect(page.getByLabel('Start offset: 01:30:00')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Remove Object and confirmation dialog @mobile', async ({ page }) => {
|
||||
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
|
||||
//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();
|
||||
//Verify both objects are in view
|
||||
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
|
||||
await expect(await page.getByLabel('Child Layout 2 Layout')).toBeVisible();
|
||||
//Remove First Object to bring up confirmation dialog
|
||||
await page.getByLabel('View menu items').nth(1).click();
|
||||
await page.getByLabel('Remove').click();
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
//Verify that the object is removed
|
||||
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
|
||||
expect(await page.getByLabel('Child Layout 2 Layout').count()).toBe(0);
|
||||
});
|
||||
});
|
@ -25,11 +25,12 @@ 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 { VISUAL_URL } from '../../../constants.js';
|
||||
|
||||
//Declare the scope of the visual test
|
||||
//Declare the component scope of the visual test for Percy
|
||||
const header = '.l-shell__head';
|
||||
|
||||
test.describe('Visual - Header @a11y', () => {
|
||||
@ -78,6 +79,26 @@ test.describe('Visual - Header @a11y', () => {
|
||||
await expect(await page.getByLabel('Show Snapshots')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
//Header test with all mission status options. Right now, this is just Mission Status, but should grow over time
|
||||
test.describe('Mission Header @a11y', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('../../../helper/addInitExampleUser.js', import.meta.url))
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByText('Select Role')).toBeVisible();
|
||||
// set role
|
||||
await page.getByRole('button', { name: 'Select', exact: true }).click();
|
||||
// dismiss role confirmation popup
|
||||
await page.getByRole('button', { name: 'Dismiss' }).click();
|
||||
});
|
||||
test('Mission status panel', async ({ page, theme }) => {
|
||||
await percySnapshot(page, `Header default with Mission Header (theme: '${theme}')`, {
|
||||
scope: header
|
||||
});
|
||||
});
|
||||
});
|
||||
// Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
// test.afterEach(async ({ page }, testInfo) => {
|
||||
// await scanForA11yViolations(page, testInfo.title);
|
||||
|
@ -28,7 +28,7 @@ import { MISSION_TIME, VISUAL_URL } from '../../../constants.js';
|
||||
//Declare the scope of the visual test
|
||||
const inspectorPane = '.l-shell__pane-inspector';
|
||||
|
||||
test.describe('Visual - Inspector @ally', () => {
|
||||
test.describe('Visual - Inspector @ally @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
@ -30,7 +30,7 @@ import percySnapshot from '@percy/playwright';
|
||||
import { MISSION_TIME, VISUAL_URL } from '../../constants.js';
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
test.describe('Visual - Controlled Clock', () => {
|
||||
test.describe('Visual - Controlled Clock @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
@ -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}')`);
|
||||
});
|
||||
});
|
||||
|
@ -20,18 +20,26 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import percySnapshot from '@percy/playwright';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import * as utils from '../../helper/faultUtils.js';
|
||||
import {
|
||||
acknowledgeFault,
|
||||
changeViewTo,
|
||||
navigateToFaultManagementWithoutExample,
|
||||
navigateToFaultManagementWithStaticExample,
|
||||
openFaultRowMenu,
|
||||
selectFaultItem,
|
||||
shelveFault
|
||||
} from '../../helper/faultUtils.js';
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
test.describe('Fault Management Visual Tests', () => {
|
||||
test('icon test', async ({ page, theme }) => {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('../../helper/addInitFaultManagementPlugin.js', import.meta.url))
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
test.describe('Fault Management Visual Tests - without example', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateToFaultManagementWithoutExample(page);
|
||||
await page.getByLabel('Collapse Inspect Pane').click();
|
||||
await page.getByLabel('Click to collapse items').click();
|
||||
});
|
||||
|
||||
test('fault management icon appears in tree', async ({ page, theme }) => {
|
||||
// Wait for status bar to load
|
||||
await expect(
|
||||
page.getByRole('status', {
|
||||
@ -51,14 +59,20 @@ test.describe('Fault Management Visual Tests', () => {
|
||||
|
||||
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Fault Management Visual Tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateToFaultManagementWithStaticExample(page);
|
||||
await page.getByLabel('Collapse Inspect Pane').click();
|
||||
await page.getByLabel('Click to collapse items').click();
|
||||
});
|
||||
|
||||
test('fault list and acknowledged faults', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await percySnapshot(page, `Shows a list of faults in the standard view (theme: '${theme}')`);
|
||||
|
||||
await utils.acknowledgeFault(page, 1);
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
await acknowledgeFault(page, 1);
|
||||
await changeViewTo(page, 'acknowledged');
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
@ -67,14 +81,12 @@ test.describe('Fault Management Visual Tests', () => {
|
||||
});
|
||||
|
||||
test('shelved faults', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await utils.shelveFault(page, 1);
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
await shelveFault(page, 1);
|
||||
await changeViewTo(page, 'shelved');
|
||||
|
||||
await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`);
|
||||
|
||||
await utils.openFaultRowMenu(page, 1);
|
||||
await openFaultRowMenu(page, 1);
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
@ -83,9 +95,7 @@ test.describe('Fault Management Visual Tests', () => {
|
||||
});
|
||||
|
||||
test('3-dot menu for fault', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await utils.openFaultRowMenu(page, 1);
|
||||
await openFaultRowMenu(page, 1);
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
@ -94,9 +104,7 @@ test.describe('Fault Management Visual Tests', () => {
|
||||
});
|
||||
|
||||
test('ability to acknowledge or shelve', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await utils.selectFaultItem(page, 1);
|
||||
await selectFaultItem(page, 1);
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
|
@ -32,12 +32,12 @@ test.describe('Mission Status Visual Tests @a11y', () => {
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByText('Select Role')).toBeVisible();
|
||||
// Description should be empty https://github.com/nasa/openmct/issues/6978
|
||||
await expect(page.locator('c-message__action-text')).toBeHidden();
|
||||
// set role
|
||||
await page.getByRole('button', { name: 'Select', exact: true }).click();
|
||||
// dismiss role confirmation popup
|
||||
await page.getByRole('button', { name: 'Dismiss' }).click();
|
||||
await page.getByLabel('Collapse Inspect Pane').click();
|
||||
await page.getByLabel('Collapse Browse Pane').click();
|
||||
});
|
||||
test('Mission status panel', async ({ page, theme }) => {
|
||||
await page.getByLabel('Toggle Mission Status Panel').click();
|
||||
|
@ -26,13 +26,41 @@ import fs from 'fs';
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
|
||||
import { test } from '../../avpFixtures.js';
|
||||
import { VISUAL_URL } from '../../constants.js';
|
||||
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
|
||||
import {
|
||||
createTimelistWithPlanAndSetActivityInProgress,
|
||||
getFirstActivity,
|
||||
setBoundsToSpanAllActivities,
|
||||
setDraftStatusForPlan
|
||||
} from '../../helper/planningUtils.js';
|
||||
|
||||
const examplePlanSmall = JSON.parse(
|
||||
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))
|
||||
);
|
||||
|
||||
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
|
||||
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 }) => {
|
||||
@ -42,42 +70,41 @@ test.describe('Visual - Planning', () => {
|
||||
test('Plan View', async ({ page, theme }) => {
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
name: 'Plan Visual Test',
|
||||
json: examplePlanSmall
|
||||
json: examplePlanSmall2
|
||||
});
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
|
||||
await percySnapshot(page, `Plan View (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
|
||||
await percySnapshot(page, `Plan View (theme: ${theme})`);
|
||||
});
|
||||
|
||||
test('Plan View w/ draft status', async ({ page, theme }) => {
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
name: 'Plan Visual Test (Draft)',
|
||||
json: examplePlanSmall
|
||||
json: examplePlanSmall2
|
||||
});
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
await setDraftStatusForPlan(page, plan);
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
|
||||
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
|
||||
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Visual - Gantt Chart', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test('Gantt Chart View', async ({ page, theme }) => {
|
||||
const ganttChart = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gantt Chart',
|
||||
name: 'Gantt Chart Visual Test'
|
||||
});
|
||||
await createPlanFromJSON(page, {
|
||||
json: examplePlanSmall,
|
||||
json: examplePlanSmall2,
|
||||
parent: ganttChart.uuid
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`);
|
||||
|
||||
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
|
||||
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
|
||||
@ -93,9 +120,7 @@ test.describe('Visual - Planning', () => {
|
||||
// Dismiss the notification
|
||||
await page.getByLabel('Dismiss').click();
|
||||
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`);
|
||||
});
|
||||
|
||||
test('Gantt Chart View w/ draft status', async ({ page, theme }) => {
|
||||
@ -104,7 +129,7 @@ test.describe('Visual - Planning', () => {
|
||||
name: 'Gantt Chart Visual Test (Draft)'
|
||||
});
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
json: examplePlanSmall,
|
||||
json: examplePlanSmall2,
|
||||
parent: ganttChart.uuid
|
||||
});
|
||||
|
||||
@ -112,10 +137,8 @@ test.describe('Visual - Planning', () => {
|
||||
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`);
|
||||
|
||||
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
|
||||
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
|
||||
@ -133,14 +156,12 @@ test.describe('Visual - Planning', () => {
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
`Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names`,
|
||||
{
|
||||
scope: snapshotScope
|
||||
}
|
||||
`Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names`
|
||||
);
|
||||
});
|
||||
// Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
// test.afterEach(async ({ page }, testInfo) => {
|
||||
// await scanForA11yViolations(page, testInfo.title);
|
||||
// });
|
||||
});
|
||||
|
||||
// Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
// test.afterEach(async ({ page }, testInfo) => {
|
||||
// await scanForA11yViolations(page, testInfo.title);
|
||||
// });
|
||||
|
@ -20,13 +20,13 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import utils from './utils.js';
|
||||
import { acknowledgeFault, randomFaults, shelveFault } from './utils.js';
|
||||
|
||||
export default function (staticFaults = false) {
|
||||
return function install(openmct) {
|
||||
openmct.install(openmct.plugins.FaultManagement());
|
||||
|
||||
const faultsData = utils.randomFaults(staticFaults);
|
||||
const faultsData = randomFaults(staticFaults);
|
||||
|
||||
openmct.faults.addProvider({
|
||||
request(domainObject, options) {
|
||||
@ -44,14 +44,14 @@ export default function (staticFaults = false) {
|
||||
return domainObject.type === 'faultManagement';
|
||||
},
|
||||
acknowledgeFault(fault, { comment = '' }) {
|
||||
utils.acknowledgeFault(fault);
|
||||
acknowledgeFault(fault);
|
||||
|
||||
return Promise.resolve({
|
||||
success: true
|
||||
});
|
||||
},
|
||||
shelveFault(fault, duration) {
|
||||
utils.shelveFault(fault, duration);
|
||||
shelveFault(fault, duration);
|
||||
|
||||
return Promise.resolve({
|
||||
success: true
|
||||
|
@ -43,7 +43,7 @@ const getRandom = {
|
||||
}
|
||||
};
|
||||
|
||||
function shelveFault(
|
||||
export function shelveFault(
|
||||
fault,
|
||||
opts = {
|
||||
shelved: true,
|
||||
@ -58,11 +58,11 @@ function shelveFault(
|
||||
}, opts.shelveDuration);
|
||||
}
|
||||
|
||||
function acknowledgeFault(fault) {
|
||||
export function acknowledgeFault(fault) {
|
||||
fault.acknowledged = true;
|
||||
}
|
||||
|
||||
function randomFaults(staticFaults, count = 5) {
|
||||
export function randomFaults(staticFaults, count = 5) {
|
||||
let faults = [];
|
||||
|
||||
for (let x = 1, y = count + 1; x < y; x++) {
|
||||
@ -71,9 +71,3 @@ function randomFaults(staticFaults, count = 5) {
|
||||
|
||||
return faults;
|
||||
}
|
||||
|
||||
export default {
|
||||
randomFaults,
|
||||
shelveFault,
|
||||
acknowledgeFault
|
||||
};
|
||||
|
@ -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
|
||||
|
27
package.json
27
package.json
@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "3.3.0-next",
|
||||
"version": "4.0.0-next",
|
||||
"description": "The Open MCT core platform",
|
||||
"type": "module",
|
||||
"main": "dist/openmct.js",
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "4.8.2",
|
||||
"@axe-core/playwright": "4.8.5",
|
||||
"@babel/eslint-parser": "7.23.3",
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@percy/cli": "1.27.4",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.39.0",
|
||||
"@types/d3-axis": "3.0.6",
|
||||
"@types/d3-shape": "3.0.0",
|
||||
"@types/d3-scale": "4.0.8",
|
||||
"@types/d3-selection": "3.0.10",
|
||||
"@types/eventemitter3": "1.2.0",
|
||||
@ -24,8 +25,9 @@
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cspell": "7.3.8",
|
||||
"css-loader": "6.8.1",
|
||||
"css-loader": "6.10.0",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-shape": "3.0.0",
|
||||
"d3-scale": "4.0.2",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.56.0",
|
||||
@ -36,7 +38,7 @@
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"eslint-plugin-unicorn": "49.0.0",
|
||||
"eslint-plugin-vue": "9.18.1",
|
||||
"eslint-plugin-vue": "9.22.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.13.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"file-saver": "2.0.5",
|
||||
@ -57,7 +59,7 @@
|
||||
"karma-webpack": "5.0.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "11.2.0",
|
||||
"marked": "12.0.0",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"moment": "2.30.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
@ -65,27 +67,27 @@
|
||||
"npm-run-all2": "6.1.1",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.87",
|
||||
"plotly.js-basic-dist-min": "2.20.0",
|
||||
"plotly.js-basic-dist-min": "2.29.1",
|
||||
"plotly.js-gl2d-dist-min": "2.20.0",
|
||||
"prettier": "3.2.5",
|
||||
"prettier-eslint": "16.3.0",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.11.0",
|
||||
"sanitize-html": "2.12.1",
|
||||
"sass": "1.68.0",
|
||||
"sass-loader": "14.0.0",
|
||||
"sass-loader": "14.1.1",
|
||||
"sinon": "17.0.0",
|
||||
"style-loader": "3.3.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"tiny-emitter": "2.1.0",
|
||||
"typescript": "5.3.3",
|
||||
"uuid": "9.0.1",
|
||||
"vue": "3.3.8",
|
||||
"vue-eslint-parser": "9.3.2",
|
||||
"vue": "3.4.19",
|
||||
"vue-eslint-parser": "9.4.2",
|
||||
"vue-loader": "16.8.3",
|
||||
"webpack": "5.89.0",
|
||||
"webpack": "5.90.3",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-dev-server": "4.15.1",
|
||||
"webpack-dev-server": "5.0.2",
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
@ -107,6 +109,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",
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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(':');
|
||||
}
|
||||
|
@ -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 (
|
||||
@ -696,15 +707,17 @@ export default class ObjectAPI {
|
||||
/**
|
||||
* Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
|
||||
* @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
|
||||
* @param {boolean} [forceRemote=false] defaults to false. If true, will skip cached and
|
||||
* dirty/in-transaction objects use and the provider.get method
|
||||
* @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
|
||||
*/
|
||||
async refresh(domainObject) {
|
||||
const refreshedObject = await this.get(domainObject.identifier);
|
||||
async refresh(domainObject, forceRemote = false) {
|
||||
const refreshedObject = await this.get(domainObject.identifier, null, forceRemote);
|
||||
|
||||
if (domainObject.isMutable) {
|
||||
domainObject.$refresh(refreshedObject);
|
||||
} else {
|
||||
utils.refresh(domainObject, refreshedObject);
|
||||
refresh(domainObject, refreshedObject);
|
||||
}
|
||||
|
||||
return domainObject;
|
||||
@ -743,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -751,7 +764,7 @@ export default class ObjectAPI {
|
||||
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object
|
||||
*/
|
||||
parseKeyString(keyString) {
|
||||
return utils.parseKeyString(keyString);
|
||||
return parseKeyString(keyString);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -759,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 &&
|
||||
@ -789,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);
|
||||
});
|
||||
@ -812,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;
|
||||
}
|
||||
@ -851,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();
|
||||
@ -864,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)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -362,7 +362,7 @@ describe('The Object API', () => {
|
||||
expect(objectAPI.get).not.toHaveBeenCalled();
|
||||
|
||||
return objectAPI.refresh(testObject).then(() => {
|
||||
expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier);
|
||||
expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier, null, false);
|
||||
|
||||
expect(testObject.otherAttribute).toEqual(OTHER_ATTRIBUTE_VALUE);
|
||||
expect(testObject.newAttribute).toEqual(NEW_ATTRIBUTE_VALUE);
|
||||
|
@ -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;
|
||||
|
@ -47,9 +47,9 @@ export default class Transaction {
|
||||
return Promise.all(promiseArray);
|
||||
}
|
||||
|
||||
createDirtyObjectPromise(object, action) {
|
||||
createDirtyObjectPromise(object, action, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
action(object)
|
||||
action(object, ...args)
|
||||
.then((success) => {
|
||||
const key = this.objectAPI.makeKeyString(object.identifier);
|
||||
|
||||
@ -75,10 +75,10 @@ export default class Transaction {
|
||||
|
||||
_clear() {
|
||||
const promiseArray = [];
|
||||
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
|
||||
const action = (obj) => this.objectAPI.refresh(obj, true);
|
||||
|
||||
Object.values(this.dirtyObjects).forEach((object) => {
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, action));
|
||||
});
|
||||
|
||||
return Promise.all(promiseArray);
|
||||
|
@ -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 &&
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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: [
|
||||
{
|
||||
|
@ -29,27 +29,29 @@
|
||||
class="c-click-icon c-overlay__close-button icon-x"
|
||||
@click.stop="destroy"
|
||||
></button>
|
||||
<div
|
||||
ref="element"
|
||||
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
|
||||
tabindex="0"
|
||||
aria-modal="true"
|
||||
aria-label="Overlay"
|
||||
role="dialog"
|
||||
></div>
|
||||
<div v-if="buttons" class="c-overlay__button-bar">
|
||||
<button
|
||||
v-for="(button, index) in buttons"
|
||||
ref="buttons"
|
||||
:key="index"
|
||||
class="c-button js-overlay__button"
|
||||
<div class="c-overlay__content-wrapper">
|
||||
<div
|
||||
ref="element"
|
||||
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
|
||||
tabindex="0"
|
||||
:class="{ 'c-button--major': focusIndex === index }"
|
||||
@focus="focusIndex = index"
|
||||
@click="buttonClickHandler(button.callback)"
|
||||
>
|
||||
{{ button.label }}
|
||||
</button>
|
||||
aria-modal="true"
|
||||
aria-label="Overlay"
|
||||
role="dialog"
|
||||
></div>
|
||||
<div v-if="buttons" class="c-overlay__button-bar">
|
||||
<button
|
||||
v-for="(button, index) in buttons"
|
||||
ref="buttons"
|
||||
:key="index"
|
||||
class="c-button js-overlay__button"
|
||||
tabindex="0"
|
||||
:class="{ 'c-button--major': focusIndex === index }"
|
||||
@focus="focusIndex = index"
|
||||
@click="buttonClickHandler(button.callback)"
|
||||
>
|
||||
{{ button.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
&__icon {
|
||||
// Holds a background SVG graphic
|
||||
$s: 80px;
|
||||
$s: 50px;
|
||||
flex: 0 0 auto;
|
||||
min-width: $s;
|
||||
min-height: $s;
|
||||
|
@ -19,7 +19,9 @@
|
||||
z-index: 70;
|
||||
|
||||
&__blocker {
|
||||
display: none; // Mobile-first
|
||||
// Mobile-first: use the blocker to create a full look to dialogs
|
||||
@include abs();
|
||||
background: $colorBodyBg;
|
||||
}
|
||||
|
||||
&__outer {
|
||||
@ -27,7 +29,13 @@
|
||||
background: $colorBodyBg;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $overlayInnerMargin;
|
||||
|
||||
body.mobile .l-overlay-fit & {
|
||||
// Vertically center small dialogs in mobile
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
@ -39,12 +47,32 @@
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
&__content-wrapper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
gap: $interiorMargin;
|
||||
|
||||
body.desktop & {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.l-overlay-fit &,
|
||||
.l-overlay-dialog & {
|
||||
margin: $overlayInnerMargin;
|
||||
}
|
||||
}
|
||||
|
||||
&__contents {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
overflow: auto;
|
||||
body.mobile & {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__top-bar {
|
||||
@ -78,6 +106,10 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: $interiorMargin;
|
||||
body.mobile & {
|
||||
justify-content: flex-end;
|
||||
padding-right: $interiorMargin;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMargin;
|
||||
|
@ -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) {
|
||||
|
@ -79,5 +79,6 @@ export default class LADTableView {
|
||||
if (this._destroy) {
|
||||
this._destroy();
|
||||
}
|
||||
this.component = null;
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ export default class ConditionSetViewProvider {
|
||||
if (_destroy) {
|
||||
_destroy();
|
||||
}
|
||||
component = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -99,7 +99,7 @@ class DisplayLayoutView {
|
||||
destroy() {
|
||||
if (this._destroy) {
|
||||
this._destroy();
|
||||
this.component = undefined;
|
||||
this.component = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
<template>
|
||||
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header c-fault-mgmt__list">
|
||||
<div class="c-fault-mgmt-item-header c-fault-mgmt__checkbox">
|
||||
<input type="checkbox" :checked="isSelectAll" @input="selectAll" />
|
||||
<input type="checkbox" :checked="isSelectAll" @change="selectAll" />
|
||||
</div>
|
||||
<div
|
||||
class="c-fault-mgmt-item-header c-fault-mgmt__list-header-results c-fault-mgmt__list-severity"
|
||||
|
@ -23,7 +23,12 @@
|
||||
<template>
|
||||
<div class="c-fault-mgmt__list data-selectable" :class="classesFromState">
|
||||
<div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
|
||||
<input type="checkbox" :checked="isSelected" @input="toggleSelected" />
|
||||
<input
|
||||
type="checkbox"
|
||||
:aria-label="checkBoxAriaLabel"
|
||||
:checked="isSelected"
|
||||
@change="toggleSelected"
|
||||
/>
|
||||
</div>
|
||||
<div class="c-fault-mgmt-item">
|
||||
<div
|
||||
@ -60,6 +65,7 @@
|
||||
<button
|
||||
class="c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots"
|
||||
title="Disposition Actions"
|
||||
aria-label="Disposition Actions"
|
||||
@click="showActionMenu"
|
||||
></button>
|
||||
</div>
|
||||
@ -86,13 +92,14 @@ export default {
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
}
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected'],
|
||||
emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected', 'clear-all-selected'],
|
||||
computed: {
|
||||
checkBoxAriaLabel() {
|
||||
return `Select fault: ${this.fault.name}`;
|
||||
},
|
||||
classesFromState() {
|
||||
const exclusiveStates = [
|
||||
{
|
||||
@ -171,6 +178,7 @@ export default {
|
||||
name: 'Acknowledge',
|
||||
description: '',
|
||||
onItemClicked: (e) => {
|
||||
this.clearAllSelected();
|
||||
this.$emit('acknowledge-selected', [this.fault]);
|
||||
}
|
||||
},
|
||||
@ -179,6 +187,7 @@ export default {
|
||||
name: 'Shelve',
|
||||
description: '',
|
||||
onItemClicked: () => {
|
||||
this.clearAllSelected();
|
||||
this.$emit('shelve-selected', [this.fault], { shelved: true });
|
||||
}
|
||||
},
|
||||
@ -188,6 +197,7 @@ export default {
|
||||
name: 'Unshelve',
|
||||
description: '',
|
||||
onItemClicked: () => {
|
||||
this.clearAllSelected();
|
||||
this.$emit('shelve-selected', [this.fault], { shelved: false });
|
||||
}
|
||||
}
|
||||
@ -202,6 +212,9 @@ export default {
|
||||
};
|
||||
|
||||
this.$emit('toggle-selected', faultData);
|
||||
},
|
||||
clearAllSelected() {
|
||||
this.$emit('clear-all-selected');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,307 +0,0 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="c-faults-list-view">
|
||||
<FaultManagementSearch
|
||||
:search-term="searchTerm"
|
||||
@filter-changed="updateFilter"
|
||||
@update-search-term="updateSearchTerm"
|
||||
/>
|
||||
|
||||
<FaultManagementToolbar
|
||||
v-if="showToolbar"
|
||||
:selected-faults="selectedFaults"
|
||||
@acknowledge-selected="toggleAcknowledgeSelected"
|
||||
@shelve-selected="toggleShelveSelected"
|
||||
/>
|
||||
|
||||
<div class="c-faults-list-view-header-item-container-wrapper">
|
||||
<div class="c-faults-list-view-header-item-container">
|
||||
<FaultManagementListHeader
|
||||
class="header"
|
||||
:selected-faults="Object.values(selectedFaults)"
|
||||
:total-faults-count="filteredFaultsList.length"
|
||||
@select-all="selectAll"
|
||||
@sort-changed="sortChanged"
|
||||
/>
|
||||
|
||||
<div class="c-faults-list-view-item-body">
|
||||
<template v-if="filteredFaultsList.length > 0">
|
||||
<FaultManagementListItem
|
||||
v-for="fault of filteredFaultsList"
|
||||
:key="fault.id"
|
||||
:fault="fault"
|
||||
:is-selected="isSelected(fault)"
|
||||
@toggle-selected="toggleSelected"
|
||||
@acknowledge-selected="toggleAcknowledgeSelected"
|
||||
@shelve-selected="toggleShelveSelected"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants.js';
|
||||
import FaultManagementListHeader from './FaultManagementListHeader.vue';
|
||||
import FaultManagementListItem from './FaultManagementListItem.vue';
|
||||
import FaultManagementSearch from './FaultManagementSearch.vue';
|
||||
import FaultManagementToolbar from './FaultManagementToolbar.vue';
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
'id',
|
||||
'triggerValueInfo',
|
||||
'currentValueInfo',
|
||||
'triggerTime',
|
||||
'severity',
|
||||
'name',
|
||||
'shortDescription',
|
||||
'namespace'
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FaultManagementListHeader,
|
||||
FaultManagementListItem,
|
||||
FaultManagementSearch,
|
||||
FaultManagementToolbar
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
faultsList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filterIndex: 0,
|
||||
searchTerm: '',
|
||||
selectedFaults: {},
|
||||
sortBy: Object.values(SORT_ITEMS)[0].value
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredFaultsList() {
|
||||
const filterName = FILTER_ITEMS[this.filterIndex];
|
||||
let list = this.faultsList;
|
||||
|
||||
// Exclude shelved alarms from all views except the Shelved view
|
||||
if (filterName !== 'Shelved') {
|
||||
list = list.filter((fault) => fault.shelved !== true);
|
||||
}
|
||||
|
||||
if (filterName === 'Acknowledged') {
|
||||
list = list.filter((fault) => fault.acknowledged);
|
||||
} else if (filterName === 'Unacknowledged') {
|
||||
list = list.filter((fault) => !fault.acknowledged);
|
||||
} else if (filterName === 'Shelved') {
|
||||
list = list.filter((fault) => fault.shelved);
|
||||
}
|
||||
|
||||
if (this.searchTerm.length > 0) {
|
||||
list = list.filter(this.filterUsingSearchTerm);
|
||||
}
|
||||
|
||||
list.sort(SORT_ITEMS[this.sortBy].sortFunction);
|
||||
|
||||
return list;
|
||||
},
|
||||
showToolbar() {
|
||||
return this.openmct.faults.supportsActions();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterUsingSearchTerm(fault) {
|
||||
if (!fault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let match = false;
|
||||
|
||||
SEARCH_KEYS.forEach((key) => {
|
||||
if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
match = true;
|
||||
}
|
||||
});
|
||||
|
||||
return match;
|
||||
},
|
||||
isSelected(fault) {
|
||||
return Boolean(this.selectedFaults[fault.id]);
|
||||
},
|
||||
selectAll(toggle = false) {
|
||||
this.faultsList.forEach((fault) => {
|
||||
const faultData = {
|
||||
fault,
|
||||
selected: toggle
|
||||
};
|
||||
this.toggleSelected(faultData);
|
||||
});
|
||||
},
|
||||
sortChanged(sort) {
|
||||
this.sortBy = sort.value;
|
||||
},
|
||||
toggleSelected({ fault, selected = false }) {
|
||||
if (selected) {
|
||||
this.selectedFaults[fault.id] = fault;
|
||||
} else {
|
||||
delete this.selectedFaults[fault.id];
|
||||
}
|
||||
|
||||
const selectedFaults = Object.values(this.selectedFaults);
|
||||
this.openmct.selection.select(
|
||||
[
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
item: this.openmct.router.path[0]
|
||||
}
|
||||
},
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
selectedFaults
|
||||
}
|
||||
}
|
||||
],
|
||||
false
|
||||
);
|
||||
},
|
||||
toggleAcknowledgeSelected(faults = Object.values(this.selectedFaults)) {
|
||||
let title = '';
|
||||
if (faults.length > 1) {
|
||||
title = `Acknowledge ${faults.length} selected faults`;
|
||||
} else {
|
||||
title = `Acknowledge fault: ${faults[0].name}`;
|
||||
}
|
||||
|
||||
const formStructure = {
|
||||
title,
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: 'comment',
|
||||
control: 'textarea',
|
||||
name: 'Optional comment',
|
||||
pattern: '\\S+',
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
buttons: {
|
||||
submit: {
|
||||
label: 'Acknowledge'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.openmct.forms.showForm(formStructure).then((data) => {
|
||||
Object.values(faults).forEach((selectedFault) => {
|
||||
this.openmct.faults.acknowledgeFault(selectedFault, data);
|
||||
});
|
||||
});
|
||||
|
||||
this.selectedFaults = {};
|
||||
},
|
||||
async toggleShelveSelected(faults = Object.values(this.selectedFaults), shelveData = {}) {
|
||||
const { shelved = true } = shelveData;
|
||||
if (shelved) {
|
||||
let title =
|
||||
faults.length > 1
|
||||
? `Shelve ${faults.length} selected faults`
|
||||
: `Shelve fault: ${faults[0].name}`;
|
||||
const formStructure = {
|
||||
title,
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: 'comment',
|
||||
control: 'textarea',
|
||||
name: 'Optional comment',
|
||||
pattern: '\\S+',
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
key: 'shelveDuration',
|
||||
control: 'select',
|
||||
name: 'Shelve duration',
|
||||
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
buttons: {
|
||||
submit: {
|
||||
label: 'Shelve'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await this.openmct.forms.showForm(formStructure);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
shelveData.comment = data.comment || '';
|
||||
shelveData.shelveDuration =
|
||||
data.shelveDuration !== undefined
|
||||
? data.shelveDuration
|
||||
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
|
||||
} else {
|
||||
shelveData = {
|
||||
shelved: false
|
||||
};
|
||||
}
|
||||
|
||||
Object.values(faults).forEach((selectedFault) => {
|
||||
this.openmct.faults.shelveFault(selectedFault, shelveData);
|
||||
});
|
||||
|
||||
this.selectedFaults = {};
|
||||
},
|
||||
updateFilter(filter) {
|
||||
this.selectAll();
|
||||
|
||||
this.filterIndex = filter.model.options.findIndex((option) => option.value === filter.value);
|
||||
},
|
||||
updateSearchTerm(term = '') {
|
||||
this.searchTerm = term.toLowerCase();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -24,20 +24,22 @@
|
||||
<div class="c-fault-mgmt__toolbar">
|
||||
<button
|
||||
class="c-icon-button icon-check"
|
||||
title="Acknowledge selected faults"
|
||||
:title="acknowledgeButtonLabel"
|
||||
:aria-label="acknowledgeButtonLabel"
|
||||
:disabled="disableAcknowledge"
|
||||
@click="acknowledgeSelected"
|
||||
>
|
||||
<div title="Acknowledge selected faults" class="c-icon-button__label">Acknowledge</div>
|
||||
<div class="c-icon-button__label">Acknowledge</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="c-icon-button icon-timer"
|
||||
title="Shelve selected faults"
|
||||
:title="shelveButtonLabel"
|
||||
:aria-label="shelveButtonLabel"
|
||||
:disabled="disableShelve"
|
||||
@click="shelveSelected"
|
||||
>
|
||||
<div title="Shelve selected items" class="c-icon-button__label">Shelve</div>
|
||||
<div class="c-icon-button__label">Shelve</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@ -60,6 +62,14 @@ export default {
|
||||
disableShelve: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
acknowledgeButtonLabel() {
|
||||
return 'Acknowledge selected faults';
|
||||
},
|
||||
shelveButtonLabel() {
|
||||
return 'Shelve selected faults';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedFaults(newSelectedFaults) {
|
||||
const selectedfaults = Object.values(newSelectedFaults);
|
||||
|
@ -21,23 +21,123 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<FaultManagementListView :faults-list="faultsList" />
|
||||
<div class="c-faults-list-view">
|
||||
<FaultManagementSearch
|
||||
:search-term="searchTerm"
|
||||
@filter-changed="updateFilter"
|
||||
@update-search-term="updateSearchTerm"
|
||||
/>
|
||||
|
||||
<FaultManagementToolbar
|
||||
v-if="showToolbar"
|
||||
:selected-faults="selectedFaults"
|
||||
@acknowledge-selected="toggleAcknowledgeSelected"
|
||||
@shelve-selected="toggleShelveSelected"
|
||||
/>
|
||||
|
||||
<div class="c-faults-list-view-header-item-container-wrapper">
|
||||
<div class="c-faults-list-view-header-item-container">
|
||||
<FaultManagementListHeader
|
||||
class="header"
|
||||
:selected-faults="selectedFaults"
|
||||
:total-faults-count="filteredFaultsList.length"
|
||||
@select-all="selectAll"
|
||||
@sort-changed="sortChanged"
|
||||
/>
|
||||
|
||||
<div class="c-faults-list-view-item-body">
|
||||
<template v-if="filteredFaultsList.length > 0">
|
||||
<FaultManagementListItem
|
||||
v-for="fault of filteredFaultsList"
|
||||
:key="fault.id"
|
||||
:fault="fault"
|
||||
:is-selected="isSelected(fault)"
|
||||
@toggle-selected="toggleSelected"
|
||||
@acknowledge-selected="toggleAcknowledgeSelected"
|
||||
@shelve-selected="toggleShelveSelected"
|
||||
@clear-all-selected="resetSelectedFaultMap"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS } from './constants.js';
|
||||
import FaultManagementListView from './FaultManagementListView.vue';
|
||||
import {
|
||||
FAULT_MANAGEMENT_ALARMS,
|
||||
FAULT_MANAGEMENT_GLOBAL_ALARMS,
|
||||
FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
|
||||
FILTER_ITEMS,
|
||||
SORT_ITEMS
|
||||
} from './constants.js';
|
||||
import FaultManagementListHeader from './FaultManagementListHeader.vue';
|
||||
import FaultManagementListItem from './FaultManagementListItem.vue';
|
||||
import FaultManagementSearch from './FaultManagementSearch.vue';
|
||||
import FaultManagementToolbar from './FaultManagementToolbar.vue';
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
'id',
|
||||
'triggerValueInfo',
|
||||
'currentValueInfo',
|
||||
'triggerTime',
|
||||
'severity',
|
||||
'name',
|
||||
'shortDescription',
|
||||
'namespace'
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FaultManagementListView
|
||||
FaultManagementListHeader,
|
||||
FaultManagementListItem,
|
||||
FaultManagementSearch,
|
||||
FaultManagementToolbar
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
faultsList: []
|
||||
faultsList: [],
|
||||
filterIndex: 0,
|
||||
searchTerm: '',
|
||||
selectedFaultMap: {},
|
||||
sortBy: Object.values(SORT_ITEMS)[0].value
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedFaults() {
|
||||
return Object.values(this.selectedFaultMap);
|
||||
},
|
||||
filteredFaultsList() {
|
||||
const filterName = FILTER_ITEMS[this.filterIndex];
|
||||
let list = this.faultsList;
|
||||
|
||||
// Exclude shelved alarms from all views except the Shelved view
|
||||
if (filterName !== 'Shelved') {
|
||||
list = list.filter((fault) => fault.shelved !== true);
|
||||
}
|
||||
|
||||
if (filterName === 'Acknowledged') {
|
||||
list = list.filter((fault) => fault.acknowledged);
|
||||
} else if (filterName === 'Unacknowledged') {
|
||||
list = list.filter((fault) => !fault.acknowledged);
|
||||
} else if (filterName === 'Shelved') {
|
||||
list = list.filter((fault) => fault.shelved);
|
||||
}
|
||||
|
||||
if (this.searchTerm.length > 0) {
|
||||
list = list.filter(this.filterUsingSearchTerm);
|
||||
}
|
||||
|
||||
list.sort(SORT_ITEMS[this.sortBy].sortFunction);
|
||||
|
||||
return list;
|
||||
},
|
||||
showToolbar() {
|
||||
return this.openmct.faults.supportsActions();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.unsubscribe = this.openmct.faults.subscribe(this.domainObject, this.updateFault);
|
||||
},
|
||||
@ -66,6 +166,181 @@ export default {
|
||||
this.faultsList = [];
|
||||
}
|
||||
});
|
||||
},
|
||||
filterUsingSearchTerm(fault) {
|
||||
if (!fault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let match = false;
|
||||
|
||||
SEARCH_KEYS.forEach((key) => {
|
||||
if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
match = true;
|
||||
}
|
||||
});
|
||||
|
||||
return match;
|
||||
},
|
||||
isSelected(fault) {
|
||||
return Boolean(this.selectedFaultMap[fault.id]);
|
||||
},
|
||||
selectAll(toggle = false) {
|
||||
this.faultsList.forEach((fault) => {
|
||||
const faultData = {
|
||||
fault,
|
||||
selected: toggle
|
||||
};
|
||||
this.toggleSelected(faultData);
|
||||
});
|
||||
},
|
||||
sortChanged(sort) {
|
||||
this.sortBy = sort.value;
|
||||
},
|
||||
toggleSelected({ fault, selected = false }) {
|
||||
if (selected) {
|
||||
this.selectedFaultMap[fault.id] = fault;
|
||||
} else {
|
||||
delete this.selectedFaultMap[fault.id];
|
||||
}
|
||||
|
||||
this.openmct.selection.select(
|
||||
[
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
item: this.openmct.router.path[0]
|
||||
}
|
||||
},
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
selectedFaults: this.selectedFaults
|
||||
}
|
||||
}
|
||||
],
|
||||
false
|
||||
);
|
||||
},
|
||||
async toggleAcknowledgeSelected(faults = this.selectedFaults) {
|
||||
let title = '';
|
||||
if (faults.length > 1) {
|
||||
title = `Acknowledge ${faults.length} selected faults`;
|
||||
} else if (faults.length === 1) {
|
||||
title = `Acknowledge fault: ${faults[0].name}`;
|
||||
}
|
||||
|
||||
const formStructure = {
|
||||
title,
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: 'comment',
|
||||
control: 'textarea',
|
||||
name: 'Optional comment',
|
||||
pattern: '\\S+',
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
buttons: {
|
||||
submit: {
|
||||
label: 'Acknowledge'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const data = await this.openmct.forms.showForm(formStructure);
|
||||
faults.forEach((fault) => {
|
||||
this.openmct.faults.acknowledgeFault(fault, data);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
this.resetSelectedFaultMap();
|
||||
}
|
||||
},
|
||||
resetSelectedFaultMap() {
|
||||
Object.keys(this.selectedFaultMap).forEach((key) => {
|
||||
delete this.selectedFaultMap[key];
|
||||
});
|
||||
},
|
||||
async toggleShelveSelected(faults = this.selectedFaults, shelveData = {}) {
|
||||
const { shelved = true } = shelveData;
|
||||
if (shelved) {
|
||||
let title =
|
||||
faults.length > 1
|
||||
? `Shelve ${faults.length} selected faults`
|
||||
: `Shelve fault: ${faults[0].name}`;
|
||||
const formStructure = {
|
||||
title,
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: 'comment',
|
||||
control: 'textarea',
|
||||
name: 'Optional comment',
|
||||
pattern: '\\S+',
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
key: 'shelveDuration',
|
||||
control: 'select',
|
||||
name: 'Shelve duration',
|
||||
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
buttons: {
|
||||
submit: {
|
||||
label: 'Shelve'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await this.openmct.forms.showForm(formStructure);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
shelveData.comment = data.comment || '';
|
||||
shelveData.shelveDuration =
|
||||
data.shelveDuration !== undefined
|
||||
? data.shelveDuration
|
||||
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
|
||||
} else {
|
||||
shelveData = {
|
||||
shelved: false
|
||||
};
|
||||
}
|
||||
|
||||
Object.values(faults).forEach((selectedFault) => {
|
||||
this.openmct.faults.shelveFault(selectedFault, shelveData);
|
||||
});
|
||||
|
||||
this.selectedFaultMap = {};
|
||||
},
|
||||
updateFilter(filter) {
|
||||
this.selectAll();
|
||||
|
||||
this.filterIndex = filter.model.options.findIndex((option) => option.value === filter.value);
|
||||
},
|
||||
updateSearchTerm(term = '') {
|
||||
this.searchTerm = term.toLowerCase();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -134,8 +134,9 @@ export default {
|
||||
this.isNested = this.options.isChildObject;
|
||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||
this.clipActivityNames = this.configuration.clipActivityNames;
|
||||
// This view is used for both gantt-chart and plan domain objects
|
||||
if (this.domainObject.type === 'plan') {
|
||||
this.setPlanData(this.domainObject);
|
||||
this.setupPlan(this.domainObject);
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
@ -143,18 +144,8 @@ export default {
|
||||
this.setDimensions();
|
||||
this.setTimeContext();
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
this.setStatus(this.openmct.status.get(this.domainObject.identifier));
|
||||
this.removeStatusListener = this.openmct.status.observe(
|
||||
this.domainObject.identifier,
|
||||
this.setStatus
|
||||
);
|
||||
this.handleConfigurationChange(this.configuration);
|
||||
this.planViewConfiguration.on('change', this.handleConfigurationChange);
|
||||
this.stopObservingSelectFile = this.openmct.objects.observe(
|
||||
this.domainObject,
|
||||
'*',
|
||||
this.handleSelectFileChange
|
||||
);
|
||||
this.loadComposition();
|
||||
},
|
||||
beforeUnmount() {
|
||||
@ -174,10 +165,25 @@ export default {
|
||||
}
|
||||
|
||||
this.planViewConfiguration.off('change', this.handleConfigurationChange);
|
||||
this.stopObservingSelectFile();
|
||||
if (this.stopObservingPlanChanges) {
|
||||
this.stopObservingPlanChanges();
|
||||
}
|
||||
this.planViewConfiguration.destroy();
|
||||
},
|
||||
methods: {
|
||||
setupPlan(domainObject) {
|
||||
this.planObject = domainObject;
|
||||
this.applyChangesForPlanObject(domainObject);
|
||||
this.stopObservingPlanChanges = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'*',
|
||||
this.applyChangesForPlanObject
|
||||
);
|
||||
this.removeStatusListener = this.openmct.status.observe(
|
||||
domainObject.identifier,
|
||||
this.setPlanStatus
|
||||
);
|
||||
},
|
||||
setPlanData(domainObject) {
|
||||
this.planData = getValidatedData(domainObject);
|
||||
},
|
||||
@ -218,8 +224,7 @@ export default {
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
this.removeFromComposition(this.planObject);
|
||||
this.planObject = domainObject;
|
||||
this.handleSelectFileChange();
|
||||
this.setupPlan(domainObject);
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
@ -237,9 +242,8 @@ export default {
|
||||
if (this.planObject) {
|
||||
this.showReplacePlanDialog(domainObject);
|
||||
} else {
|
||||
this.planObject = domainObject;
|
||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||
this.handleSelectFileChange(domainObject);
|
||||
this.setupPlan(domainObject);
|
||||
}
|
||||
},
|
||||
handleConfigurationChange(newConfiguration) {
|
||||
@ -259,10 +263,10 @@ export default {
|
||||
|
||||
this.setScaleAndGenerateActivities();
|
||||
},
|
||||
handleSelectFileChange(domainObject) {
|
||||
applyChangesForPlanObject(domainObject) {
|
||||
const planDomainObject = domainObject || this.domainObject;
|
||||
this.setPlanData(planDomainObject);
|
||||
this.setStatus(this.openmct.status.get(planDomainObject.identifier));
|
||||
this.setPlanStatus(this.openmct.status.get(planDomainObject.identifier));
|
||||
this.setScaleAndGenerateActivities();
|
||||
},
|
||||
removeFromComposition(domainObject) {
|
||||
@ -568,7 +572,7 @@ export default {
|
||||
swimlaneWidth
|
||||
};
|
||||
},
|
||||
setStatus(status) {
|
||||
setPlanStatus(status) {
|
||||
this.status = status;
|
||||
},
|
||||
getClipPathId(groupName, activity, row) {
|
||||
|
@ -53,13 +53,13 @@
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<mct-ticks
|
||||
<MctTicks
|
||||
v-show="gridLines && !options.compact"
|
||||
:axis-type="'xAxis'"
|
||||
:position="'right'"
|
||||
/>
|
||||
|
||||
<mct-ticks
|
||||
<MctTicks
|
||||
v-for="(yAxis, index) in yAxesIds"
|
||||
v-show="gridLines"
|
||||
:key="`yAxis-gridlines-${index}`"
|
||||
|
@ -273,12 +273,14 @@ export default {
|
||||
)
|
||||
);
|
||||
|
||||
this.tickWidth = tickWidth;
|
||||
this.$emit('plot-tick-width', {
|
||||
width: tickWidth,
|
||||
yAxisId: this.axisType === 'yAxis' ? this.axisId : ''
|
||||
});
|
||||
this.shouldCheckWidth = false;
|
||||
if (this.tickWidth !== tickWidth) {
|
||||
this.tickWidth = tickWidth;
|
||||
this.$emit('plot-tick-width', {
|
||||
width: tickWidth,
|
||||
yAxisId: this.axisType === 'yAxis' ? this.axisId : ''
|
||||
});
|
||||
this.shouldCheckWidth = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,10 +201,15 @@ export default {
|
||||
|
||||
if (this.compositionCollection) {
|
||||
this.compositionCollection.on('add', this.subscribeToStaleness);
|
||||
this.compositionCollection.on('remove', this.triggerUnsubscribeFromStaleness);
|
||||
this.compositionCollection.on('remove', this.removeSubscription);
|
||||
this.compositionCollection.load();
|
||||
}
|
||||
},
|
||||
removeSubscription(identifier) {
|
||||
this.triggerUnsubscribeFromStaleness({
|
||||
identifier
|
||||
});
|
||||
},
|
||||
loadingUpdated(loading) {
|
||||
this.loading = loading;
|
||||
this.$emit('loading-updated', ...arguments);
|
||||
@ -212,7 +217,7 @@ export default {
|
||||
destroy() {
|
||||
if (this.compositionCollection) {
|
||||
this.compositionCollection.off('add', this.subscribeToStaleness);
|
||||
this.compositionCollection.off('remove', this.triggerUnsubscribeFromStaleness);
|
||||
this.compositionCollection.off('remove', this.removeSubscription);
|
||||
}
|
||||
|
||||
this.imageExporter = null;
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
<template>
|
||||
<div v-if="loaded" class="gl-plot-axis-area gl-plot-x has-local-controls">
|
||||
<mct-ticks :axis-type="'xAxis'" :position="'left'" @plot-tick-width="onTickWidthChange" />
|
||||
<MctTicks :axis-type="'xAxis'" :position="'left'" />
|
||||
|
||||
<div class="gl-plot-label gl-plot-x-label" :class="{ 'icon-gear': isEnabledXKeyToggle() }">
|
||||
{{ xAxisLabel }}
|
||||
@ -59,7 +59,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: ['plot-x-tick-width'],
|
||||
data() {
|
||||
return {
|
||||
selectedXKeyOptionKey: '',
|
||||
@ -137,9 +136,6 @@ export default {
|
||||
this.xAxisLabel = this.xAxis.get('label');
|
||||
this.selectedXKeyOptionKey =
|
||||
this.xKeyOptions.length > 0 ? this.getXKeyOption(xAxisKey).key : xAxisKey;
|
||||
},
|
||||
onTickWidthChange(width) {
|
||||
this.$emit('plot-x-tick-width', width);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -60,7 +60,7 @@
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<mct-ticks
|
||||
<MctTicks
|
||||
:axis-id="id"
|
||||
:axis-type="'yAxis'"
|
||||
class="gl-plot-ticks"
|
||||
|
@ -39,13 +39,13 @@
|
||||
<div ref="limitArea" class="js-limit-area" aria-hidden="true">
|
||||
<limit-label
|
||||
v-for="(limitLabel, index) in visibleLimitLabels"
|
||||
:key="index"
|
||||
:key="`limitLabel-${limitLabel.limit.seriesKey}-${index}`"
|
||||
:point="limitLabel.point"
|
||||
:limit="limitLabel.limit"
|
||||
></limit-label>
|
||||
<limit-line
|
||||
v-for="(limitLine, index) in visibleLimitLines"
|
||||
:key="index"
|
||||
:key="`limitLine-${limitLine.limit.seriesKey}${index}`"
|
||||
:point="limitLine.point"
|
||||
:limit="limitLine.limit"
|
||||
></limit-line>
|
||||
@ -201,9 +201,8 @@ export default {
|
||||
handler() {
|
||||
this.hiddenYAxisIds.forEach((id) => {
|
||||
this.resetYOffsetAndSeriesDataForYAxis(id);
|
||||
this.updateLimitLines();
|
||||
});
|
||||
this.scheduleDraw();
|
||||
this.scheduleDraw(true);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
@ -271,12 +270,19 @@ export default {
|
||||
this.listenTo(this.config.xAxis, 'change', this.redrawIfNotAlreadyHandled);
|
||||
this.config.series.forEach(this.onSeriesAdd, this);
|
||||
this.$emit('chart-loaded');
|
||||
|
||||
this.handleWindowResize = _.debounce(this.handleWindowResize, 250);
|
||||
this.chartResizeObserver = new ResizeObserver(this.handleWindowResize);
|
||||
this.chartResizeObserver.observe(this.$parent.$refs.chartContainer);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.destroy();
|
||||
this.visibilityObserver.unobserve(this.chartContainer);
|
||||
},
|
||||
methods: {
|
||||
handleWindowResize() {
|
||||
this.scheduleDraw(true);
|
||||
},
|
||||
getConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
@ -445,7 +451,6 @@ export default {
|
||||
|
||||
this.makeLimitLines(series);
|
||||
this.updateLimitLines();
|
||||
this.scheduleDraw();
|
||||
},
|
||||
resetAxisAndRedraw(newYAxisId, oldYAxisId, series) {
|
||||
if (!oldYAxisId) {
|
||||
@ -459,8 +464,7 @@ export default {
|
||||
//Make the chart elements again for the new y-axis and offset
|
||||
this.makeChartElement(series);
|
||||
this.makeLimitLines(series);
|
||||
this.updateLimitLines();
|
||||
this.scheduleDraw();
|
||||
this.scheduleDraw(true);
|
||||
},
|
||||
destroy() {
|
||||
this.destroyCanvas();
|
||||
@ -469,6 +473,10 @@ export default {
|
||||
this.limitLines.forEach((line) => line.destroy());
|
||||
this.pointSets.forEach((pointSet) => pointSet.destroy());
|
||||
this.alarmSets.forEach((alarmSet) => alarmSet.destroy());
|
||||
DrawLoader.releaseDrawAPI(this.drawAPI);
|
||||
if (this.chartResizeObserver) {
|
||||
this.chartResizeObserver.disconnect();
|
||||
}
|
||||
},
|
||||
resetYOffsetAndSeriesDataForYAxis(yAxisId) {
|
||||
delete this.offset[yAxisId].y;
|
||||
@ -703,12 +711,11 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateLimitLines();
|
||||
this.scheduleDraw();
|
||||
this.scheduleDraw(true);
|
||||
},
|
||||
scheduleDraw() {
|
||||
scheduleDraw(updateLimitLines) {
|
||||
if (!this.drawScheduled) {
|
||||
const called = this.renderWhenVisible(this.draw);
|
||||
const called = this.renderWhenVisible(this.draw.bind(this, updateLimitLines));
|
||||
this.drawScheduled = called;
|
||||
if (!this.drawnOnce && called) {
|
||||
this.drawnOnce = true;
|
||||
@ -716,7 +723,7 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
draw() {
|
||||
draw(updateLimitLines) {
|
||||
this.drawScheduled = false;
|
||||
if (this.isDestroyed || !this.chartVisible) {
|
||||
return;
|
||||
@ -744,6 +751,11 @@ export default {
|
||||
this.prepareToDrawAnnotationSelections(id);
|
||||
}
|
||||
});
|
||||
// We must do the limit line drawing after the drawAPI has been cleared (which sets the height and width of the draw API)
|
||||
// and the viewport is updated so that we have the right height/width for limit line x and y calculations
|
||||
if (updateLimitLines) {
|
||||
this.updateLimitLines();
|
||||
}
|
||||
},
|
||||
updateViewport(yAxisId) {
|
||||
if (!this.chartVisible) {
|
||||
@ -799,9 +811,12 @@ export default {
|
||||
pointSets.forEach(this.drawPoints, this);
|
||||
const alarmSets = this.alarmSets.filter(this.matchByYAxisId.bind(this, id));
|
||||
alarmSets.forEach(this.drawAlarmPoints, this);
|
||||
//console.timeEnd('📈 drawSeries');
|
||||
},
|
||||
updateLimitLines() {
|
||||
//reset
|
||||
this.visibleLimitLabels = [];
|
||||
this.visibleLimitLines = [];
|
||||
|
||||
this.config.series.models.forEach((series) => {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
|
||||
@ -820,11 +835,7 @@ export default {
|
||||
if (!this.drawAPI.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
let limitPointOverlap = [];
|
||||
//reset
|
||||
this.visibleLimitLabels = [];
|
||||
this.visibleLimitLines = [];
|
||||
|
||||
this.limitLines.forEach((limitLine) => {
|
||||
limitLine.limits.forEach((limit) => {
|
||||
|
@ -23,7 +23,7 @@
|
||||
<div v-if="loaded" class="js-plot-options-browse">
|
||||
<ul v-if="!isStackedPlotObject" class="c-tree" aria-label="Plot Series Properties">
|
||||
<h2 class="--first" title="Plot series display properties in this object">Plot Series</h2>
|
||||
<plot-options-item v-for="series in plotSeries" :key="series.key" :series="series" />
|
||||
<PlotOptionsItem v-for="series in plotSeries" :key="series.keyString" :series="series" />
|
||||
</ul>
|
||||
<div v-if="plotSeries.length && !isStackedPlotObject" class="grid-properties">
|
||||
<ul
|
||||
|
@ -23,7 +23,7 @@
|
||||
<div v-if="loaded" class="js-plot-options-edit">
|
||||
<ul v-if="!isStackedPlotObject" class="c-tree" aria-label="Plot Series Properties">
|
||||
<h2 class="--first" title="Display properties for this object">Plot Series</h2>
|
||||
<li v-for="series in plotSeries" :key="series.key">
|
||||
<li v-for="series in plotSeries" :key="series.keyString">
|
||||
<series-form :series="series" @series-updated="updateSeriesConfigForObject" />
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -184,7 +184,7 @@ export default {
|
||||
return this.series.get('color').asHexString();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
created() {
|
||||
this.status = this.openmct.status.get(this.series.domainObject.identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(
|
||||
this.series.domainObject.identifier,
|
||||
|
@ -91,9 +91,16 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div class="grid-cell label" title="Display limit lines">Limit lines</div>
|
||||
<div id="limit-lines-checkbox" class="grid-cell label" title="Display limit lines">
|
||||
Limit lines
|
||||
</div>
|
||||
<div class="grid-cell value">
|
||||
<input v-model="limitLines" type="checkbox" @change="updateForm('limitLines')" />
|
||||
<input
|
||||
v-model="limitLines"
|
||||
aria-labelledby="limit-lines-checkbox"
|
||||
type="checkbox"
|
||||
@change="updateForm('limitLines')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li v-show="markers || alarmMarkers" class="grid-row">
|
||||
@ -191,7 +198,7 @@ export default {
|
||||
return this.series.get('color').asHexString();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
created() {
|
||||
this.initialize();
|
||||
|
||||
this.status = this.openmct.status.get(this.series.domainObject.identifier);
|
||||
@ -206,7 +213,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initialize: function () {
|
||||
initialize() {
|
||||
this.fields = [
|
||||
{
|
||||
modelProp: 'yKey',
|
||||
|
@ -165,7 +165,6 @@ export default {
|
||||
this.registerListeners(this.config);
|
||||
}
|
||||
this.listenTo(this.config.legend, 'change:expandByDefault', this.changeExpandDefault, this);
|
||||
this.initialize();
|
||||
},
|
||||
mounted() {
|
||||
this.loaded = true;
|
||||
@ -182,16 +181,6 @@ export default {
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
initialize() {
|
||||
if (this.domainObject.type === 'telemetry.plot.stacked') {
|
||||
this.objectComposition = this.openmct.composition.get(this.domainObject);
|
||||
this.objectComposition.on('add', this.addTelemetryObject);
|
||||
this.objectComposition.on('remove', this.removeTelemetryObject);
|
||||
this.objectComposition.load();
|
||||
} else {
|
||||
this.registerListeners(this.config);
|
||||
}
|
||||
},
|
||||
changeExpandDefault() {
|
||||
this.isLegendExpanded = this.config.legend.model.expandByDefault;
|
||||
this.legend.set('expanded', this.isLegendExpanded);
|
||||
|
@ -22,7 +22,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="plot-legend-item"
|
||||
:aria-label="`Plot Legend Item for ${domainObject?.name}`"
|
||||
:aria-label="`Plot Legend Item for ${seriesName}`"
|
||||
:class="{
|
||||
'is-stale': isStale,
|
||||
'is-status--missing': isMissing
|
||||
@ -36,9 +36,8 @@
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<span class="plot-series-color-swatch" :style="{ 'background-color': colorAsHexString }">
|
||||
</span>
|
||||
<span class="is-status__indicator" title="This item is missing or suspect"></span>
|
||||
<span class="plot-series-color-swatch" :style="{ 'background-color': colorAsHexString }" />
|
||||
<span class="is-status__indicator" title="This item is missing or suspect" />
|
||||
<span class="plot-series-name">{{ nameWithUnit }}</span>
|
||||
</div>
|
||||
<div
|
||||
@ -89,6 +88,7 @@ export default {
|
||||
isMissing: false,
|
||||
colorAsHexString: '',
|
||||
nameWithUnit: '',
|
||||
seriesName: '',
|
||||
formattedYValue: '',
|
||||
formattedXValue: '',
|
||||
mctLimitStateClass: '',
|
||||
@ -206,6 +206,9 @@ export default {
|
||||
const seriesIndexToRemove = this.seriesModels.findIndex(
|
||||
(series) => series.keyString === seriesToRemove.keyString
|
||||
);
|
||||
if (seriesIndexToRemove === -1) {
|
||||
return;
|
||||
}
|
||||
this.seriesModels.splice(seriesIndexToRemove, 1);
|
||||
},
|
||||
getSeries(keyStringToFind) {
|
||||
@ -220,6 +223,7 @@ export default {
|
||||
|
||||
this.isMissing = seriesObject.domainObject.status === 'missing';
|
||||
this.colorAsHexString = seriesObject.get('color').asHexString();
|
||||
this.seriesName = seriesObject.domainObject.name;
|
||||
this.nameWithUnit = seriesObject.nameWithUnit();
|
||||
|
||||
const closest = seriesObject.closest;
|
||||
|
@ -135,7 +135,7 @@ export default {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
if (this.composition) {
|
||||
this.composition.off('add', this.subscribeToStaleness);
|
||||
this.composition.off('remove', this.triggerUnsubscribeFromStaleness);
|
||||
this.composition.off('remove', this.removeSubscription);
|
||||
}
|
||||
|
||||
if (this.removeSelectable) {
|
||||
@ -161,6 +161,11 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
removeSubscription(identifier) {
|
||||
this.triggerUnsubscribeFromStaleness({
|
||||
identifier
|
||||
});
|
||||
},
|
||||
updateView() {
|
||||
//If this object is not persistable, then package it with it's parent
|
||||
const plotObject = this.getPlotObject();
|
||||
@ -172,7 +177,7 @@ export default {
|
||||
this.composition = this.openmct.composition.get(plotObject);
|
||||
|
||||
this.composition.on('add', this.subscribeToStaleness);
|
||||
this.composition.on('remove', this.triggerUnsubscribeFromStaleness);
|
||||
this.composition.on('remove', this.removeSubscription);
|
||||
this.composition.load();
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,10 @@ export default {
|
||||
this.stopListeningStyles();
|
||||
}
|
||||
|
||||
if (this.stopListeningFontStyles) {
|
||||
this.stopListeningFontStyles();
|
||||
}
|
||||
|
||||
if (this.styleRuleManager) {
|
||||
this.styleRuleManager.destroy();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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 = {
|
||||
|
@ -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];
|
||||
};
|
||||
|
||||
|
@ -82,6 +82,7 @@ export default class Tabs {
|
||||
if (this.destroy) {
|
||||
this.destroy();
|
||||
}
|
||||
component = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -63,6 +63,7 @@ export default class TelemetryTableView {
|
||||
if (this._destroy) {
|
||||
this._destroy();
|
||||
}
|
||||
this.component = null;
|
||||
}
|
||||
|
||||
show(element, editMode, { renderWhenVisible }) {
|
||||
|
@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<form ref="fixedDeltaInput">
|
||||
<div class="c-tc-input-popup__input-grid">
|
||||
<div class="pr-time-label"><em>Start</em> Date</div>
|
||||
<div class="pr-time-label">Time</div>
|
||||
<div class="pr-time-label"></div>
|
||||
<div class="pr-time-label"><em>End</em> Date</div>
|
||||
<div class="pr-time-label">Time</div>
|
||||
<div class="pr-time-label"></div>
|
||||
<div class="pr-time-label pr-time-label-start-date"><em>Start</em> Date</div>
|
||||
<div class="pr-time-label pr-time-label-start-time">Time</div>
|
||||
<div class="pr-time-label pr-time-label-end-date"><em>End</em> Date</div>
|
||||
<div class="pr-time-label pr-time-label-end-time">Time</div>
|
||||
|
||||
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
|
||||
<div
|
||||
class="pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-start-date"
|
||||
>
|
||||
<input
|
||||
ref="startDate"
|
||||
v-model="formattedBounds.start"
|
||||
@ -28,7 +28,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pr-time-input pr-time-input--time">
|
||||
<div class="pr-time-input pr-time-input--time pr-time-input-start-time">
|
||||
<input
|
||||
ref="startTime"
|
||||
v-model="formattedBounds.startTime"
|
||||
@ -43,7 +43,9 @@
|
||||
|
||||
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
|
||||
|
||||
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
|
||||
<div
|
||||
class="pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-end-date"
|
||||
>
|
||||
<input
|
||||
ref="endDate"
|
||||
v-model="formattedBounds.end"
|
||||
@ -63,7 +65,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pr-time-input pr-time-input--time">
|
||||
<div class="pr-time-input pr-time-input--time pr-time-input-end-time">
|
||||
<input
|
||||
ref="endTime"
|
||||
v-model="formattedBounds.endTime"
|
||||
|
@ -1,16 +1,14 @@
|
||||
<template>
|
||||
<form ref="deltaInput">
|
||||
<div class="c-tc-input-popup__input-grid">
|
||||
<div class="pr-time-label icon-minus">Hrs</div>
|
||||
<div class="pr-time-label">Mins</div>
|
||||
<div class="pr-time-label">Secs</div>
|
||||
<div class="pr-time-label"></div>
|
||||
<div class="pr-time-label icon-plus">Hrs</div>
|
||||
<div class="pr-time-label">Mins</div>
|
||||
<div class="pr-time-label">Secs</div>
|
||||
<div class="pr-time-label"></div>
|
||||
<div class="pr-time-label icon-minus pr-time-label-minus-hrs">Hrs</div>
|
||||
<div class="pr-time-label pr-time-label-minus-mins">Mins</div>
|
||||
<div class="pr-time-label pr-time-label-minus-secs">Secs</div>
|
||||
<div class="pr-time-label icon-plus pr-time-label-plus-hrs">Hrs</div>
|
||||
<div class="pr-time-label pr-time-label-plus-mins">Mins</div>
|
||||
<div class="pr-time-label pr-time-label-plus-secs">Secs</div>
|
||||
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-minus-hrs">
|
||||
<input
|
||||
ref="startInputHrs"
|
||||
v-model="startInputHrs"
|
||||
@ -29,7 +27,7 @@
|
||||
/>
|
||||
<b>:</b>
|
||||
</div>
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-minus-mins">
|
||||
<input
|
||||
ref="startInputMins"
|
||||
v-model="startInputMins"
|
||||
@ -48,7 +46,7 @@
|
||||
/>
|
||||
<b>:</b>
|
||||
</div>
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-minus-secs">
|
||||
<input
|
||||
ref="startInputSecs"
|
||||
v-model="startInputSecs"
|
||||
@ -69,7 +67,7 @@
|
||||
|
||||
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
|
||||
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-plus-hrs">
|
||||
<input
|
||||
ref="endInputHrs"
|
||||
v-model="endInputHrs"
|
||||
@ -88,7 +86,7 @@
|
||||
/>
|
||||
<b>:</b>
|
||||
</div>
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-plus-mins">
|
||||
<input
|
||||
ref="endInputMins"
|
||||
v-model="endInputMins"
|
||||
@ -106,7 +104,7 @@
|
||||
/>
|
||||
<b>:</b>
|
||||
</div>
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-plus-secs">
|
||||
<input
|
||||
ref="endInputSecs"
|
||||
v-model="endInputSecs"
|
||||
|
@ -604,23 +604,170 @@
|
||||
padding: cButtonPadding($compact: true);
|
||||
}
|
||||
}
|
||||
.pr-time{
|
||||
// FIXED TIME MODE
|
||||
&-label-start-date{
|
||||
grid-area: sDate;
|
||||
}
|
||||
&-label-start-time{
|
||||
grid-area: sTime;
|
||||
}
|
||||
&-input-start-date{
|
||||
grid-area: sDateInput;
|
||||
}
|
||||
&-input-start-time{
|
||||
grid-area: sTimeInput;
|
||||
}
|
||||
&-label-end-date{
|
||||
grid-area: eDate;
|
||||
}
|
||||
&-label-end-time{
|
||||
grid-area: eTime;
|
||||
|
||||
}
|
||||
&-input-end-date{
|
||||
grid-area: eDateInput;
|
||||
}
|
||||
&-input-end-time{
|
||||
grid-area: eTimeInput;
|
||||
}
|
||||
&-label-blank-grid{
|
||||
grid-area: blank;
|
||||
}
|
||||
|
||||
//REAL TIME MODE
|
||||
&-label-minus-hrs{
|
||||
grid-area: labelMinusHrs;
|
||||
}
|
||||
&-label-minus-mins{
|
||||
grid-area: labelMinusMins;
|
||||
}
|
||||
&-label-minus-secs{
|
||||
grid-area: labelMinusSecs;
|
||||
}
|
||||
&-label-plus-hrs{
|
||||
grid-area: labelPlusHrs;
|
||||
}
|
||||
&-label-plus-mins{
|
||||
grid-area: labelPlusMins;
|
||||
}
|
||||
&-label-plus-secs{
|
||||
grid-area: labelPlusSecs;
|
||||
}
|
||||
&-input-minus-hrs{
|
||||
grid-area: inputMinusHrs;
|
||||
}
|
||||
&-input-minus-mins{
|
||||
grid-area: inputMinusMins;
|
||||
}
|
||||
&-input-minus-secs{
|
||||
grid-area: inputMinusSecs;
|
||||
}
|
||||
&-input-plus-hrs{
|
||||
grid-area: inputPlusHrs;
|
||||
}
|
||||
&-input-plus-mins{
|
||||
grid-area: inputPlusMins;
|
||||
}
|
||||
&-input-plus-secs{
|
||||
grid-area: inputPlusSecs;
|
||||
}
|
||||
// USED FOR BOTH
|
||||
&-label-blank-grid{
|
||||
grid-area: empty;
|
||||
}
|
||||
&-input__start-end-sep{
|
||||
grid-area: arrowIcon;
|
||||
}
|
||||
&-input--buttons{
|
||||
grid-area: buttons;
|
||||
}
|
||||
}
|
||||
|
||||
&--fixed-mode {
|
||||
.c-tc-input-popup__input-grid {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 2fr;
|
||||
grid-template-areas:
|
||||
"sDate sTime . eDate eTime ."
|
||||
"sDateInput sTimeInput arrowIcon eDateInput eTimeInput buttons";
|
||||
}
|
||||
@include phonePortrait(){
|
||||
.c-tc-input-popup__input-grid {
|
||||
grid-template-columns: repeat(2, max-content) 1fr;
|
||||
grid-template-areas:
|
||||
"sDate sTime ."
|
||||
"sDateInput sTimeInput ."
|
||||
"eDate eTime ."
|
||||
"eDateInput eTimeInput buttons";
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--realtime-mode {
|
||||
.c-tc-input-popup__input-grid {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
|
||||
grid-template-areas:
|
||||
"labelMinusHrs labelMinusMins labelMinusSecs . labelPlusHrs labelPlusMins labelPlusSecs ."
|
||||
"inputMinusHrs inputMinusMins inputMinusSecs arrowIcon inputPlusHrs inputPlusMins inputPlusSecs buttons";
|
||||
}
|
||||
@include phonePortrait(){
|
||||
.c-tc-input-popup__input-grid {
|
||||
grid-template-columns: repeat(3, max-content) 1fr;
|
||||
grid-template-areas:
|
||||
"labelMinusHrs labelMinusMins labelMinusSecs ."
|
||||
"inputMinusHrs inputMinusMins inputMinusSecs ."
|
||||
"labelPlusHrs labelPlusMins labelPlusSecs ."
|
||||
"inputPlusHrs inputPlusMins inputPlusSecs buttons";
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__input-grid {
|
||||
display: grid;
|
||||
grid-column-gap: 3px;
|
||||
grid-row-gap: $interiorMargin;
|
||||
grid-column-gap: $interiorMarginSm;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
@include phonePortrait(){ // Additional styling for mobile portrait.
|
||||
.c-tc-input-popup{
|
||||
width: 100%;
|
||||
&__options{
|
||||
> * {
|
||||
overflow: hidden;
|
||||
[class*= 'ctrl-wrapper']{
|
||||
[class*='--menu'] {
|
||||
width: 100%;
|
||||
[class*='__label'] {
|
||||
@include ellipsize();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pr-time-input-end-time, .pr-time-input-start-time{
|
||||
> * {
|
||||
margin-right: $interiorMargin;
|
||||
}
|
||||
}
|
||||
.pr-time-input--buttons{
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.pr-time-input__start-end-sep{
|
||||
margin: auto;
|
||||
}
|
||||
.pr-time-input__start-end-sep{
|
||||
display: none;
|
||||
}
|
||||
.pr-time-input-start-date, .pr-time-input-end-date{
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,18 @@
|
||||
</div>
|
||||
<div class="c-tli__graphic">
|
||||
<svg viewBox="0 0 100 100">
|
||||
<g aria-label="Activity in progress" class="c-tli__graphic__pie">
|
||||
<circle class="c-svg-progress__bg" r="50" cx="50" cy="50"></circle>
|
||||
<path ref="progressElement" class="c-svg-progress__progress"></path>
|
||||
<circle
|
||||
class="c-svg-progress__ticks"
|
||||
r="40"
|
||||
cx="50"
|
||||
cy="50"
|
||||
stroke-dasharray="3 7.472"
|
||||
></circle>
|
||||
<rect class="c-svg-progress__sweep-hand" x="48" y="18" width="4" height="27"></rect>
|
||||
</g>
|
||||
<path
|
||||
aria-label="Activity complete"
|
||||
class="c-tli__graphic__check"
|
||||
@ -80,6 +92,7 @@ import _ from 'lodash';
|
||||
|
||||
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
|
||||
import { CURRENT_CSS_SUFFIX, FUTURE_CSS_SUFFIX, PAST_CSS_SUFFIX } from './constants.js';
|
||||
import { updateProgress } from './svg-progress.js';
|
||||
|
||||
const ITEM_COLORS = {
|
||||
[CURRENT_CSS_SUFFIX]: '#ffcc00',
|
||||
@ -212,6 +225,7 @@ export default {
|
||||
},
|
||||
followTimeContext() {
|
||||
this.timeContext.on(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp);
|
||||
this.updateTimestamp(this.timeContext.now());
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
@ -220,6 +234,10 @@ export default {
|
||||
},
|
||||
updateTimestamp(time) {
|
||||
this.timestamp = time;
|
||||
const progressElement = this.$refs.progressElement;
|
||||
if (this.isInProgress && progressElement) {
|
||||
updateProgress(this.start, this.end, this.timestamp, progressElement);
|
||||
}
|
||||
this.formatItemLabel();
|
||||
},
|
||||
formatItemLabel() {
|
||||
|
@ -63,6 +63,7 @@
|
||||
<list-item
|
||||
v-for="item in sortedItems"
|
||||
:key="item.key"
|
||||
:class="{ '--is-in-progress': persistedActivityStates[item.id] === 'in-progress' }"
|
||||
:item="item"
|
||||
:item-properties="itemProperties"
|
||||
@click.stop="setSelectionForActivity(item, $event.currentTarget)"
|
||||
@ -85,8 +86,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 +208,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 +236,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 +268,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 +420,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 +498,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 +523,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];
|
||||
|
49
src/plugins/timelist/svg-progress.js
Normal file
49
src/plugins/timelist/svg-progress.js
Normal file
@ -0,0 +1,49 @@
|
||||
const PI = Math.PI; // Use the built-in constant directly
|
||||
const DEGREES_TO_RADIANS = PI / 180; // Calculate the conversion factor
|
||||
|
||||
import { arc } from 'd3-shape';
|
||||
|
||||
const SVG_VB_SIZE = 100;
|
||||
const UPDATE_RATE_MS = 1000; // 1 Hz
|
||||
|
||||
function progToDegrees(progVal) {
|
||||
return (progVal / 100) * 360;
|
||||
}
|
||||
|
||||
function renderProgress(progressPercent, element) {
|
||||
let startAngleInDegrees = 0;
|
||||
let endAngleInDegrees = progToDegrees(progressPercent);
|
||||
|
||||
// Convert angles to radians for calculations
|
||||
const startAngleInRadians = startAngleInDegrees * DEGREES_TO_RADIANS;
|
||||
const endAngleInRadians = endAngleInDegrees * DEGREES_TO_RADIANS;
|
||||
|
||||
// d3's arc API does the work for us
|
||||
const progressArc = arc();
|
||||
progressArc.innerRadius(0);
|
||||
progressArc.outerRadius(SVG_VB_SIZE / 2);
|
||||
progressArc.startAngle(startAngleInRadians);
|
||||
progressArc.endAngle(endAngleInRadians);
|
||||
element.setAttribute('d', progressArc());
|
||||
}
|
||||
|
||||
export function updateProgress(start, end, timestamp, element) {
|
||||
const duration = end - start;
|
||||
const update_per_cycle = 100 / (duration / UPDATE_RATE_MS);
|
||||
let progressPercent = 0;
|
||||
if (timestamp > start) {
|
||||
// Now is after activity start datetime
|
||||
if (timestamp > end) {
|
||||
progressPercent = 100;
|
||||
} else {
|
||||
progressPercent = (1 - (end - timestamp) / duration) * 100;
|
||||
}
|
||||
}
|
||||
if (progressPercent < 100 && progressPercent > 0) {
|
||||
// If the remaining percent is less than update_per_cycle, round up to 100%.
|
||||
// Otherwise, increment by update_per_cycle.
|
||||
progressPercent =
|
||||
100 - progressPercent < update_per_cycle ? 100 : (progressPercent += update_per_cycle);
|
||||
}
|
||||
renderProgress(progressPercent, element);
|
||||
}
|
@ -29,7 +29,15 @@
|
||||
}
|
||||
|
||||
.c-list-item {
|
||||
/* Time Lists */
|
||||
/* Compact Time Lists; is a <tr> element */
|
||||
|
||||
@mixin sSelected($bgColor, $fgColor) {
|
||||
&[s-selected] {
|
||||
background: $bgColor !important;
|
||||
border: 1px solid $colorSelectedFg !important;
|
||||
color: $fgColor !important;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
$p: $interiorMarginSm;
|
||||
@ -37,19 +45,26 @@
|
||||
padding-bottom: $p;
|
||||
}
|
||||
|
||||
&.--is-past {
|
||||
@include sSelected(transparent, $colorPastFgEm);
|
||||
}
|
||||
|
||||
&.--is-current {
|
||||
@include sSelected($colorCurrentBg, $colorCurrentFgEm);
|
||||
background-color: $colorCurrentBg;
|
||||
border-top: 1px solid $colorCurrentBorder !important;
|
||||
color: $colorCurrentFgEm;
|
||||
}
|
||||
|
||||
&.--is-future {
|
||||
@include sSelected($colorFutureBg, $colorFutureFgEm);
|
||||
background-color: $colorFutureBg;
|
||||
border-top-color: $colorFutureBorder !important;
|
||||
color: $colorFutureFgEm;
|
||||
}
|
||||
|
||||
&.--is-in-progress {
|
||||
@include sSelected($colorInProgressBg, $colorInProgressFgEm);
|
||||
background-color: $colorInProgressBg;
|
||||
}
|
||||
|
||||
@ -105,9 +120,10 @@
|
||||
grid-column-gap: $interiorMargin;
|
||||
|
||||
&[s-selected] {
|
||||
background: $colorSelectedBg !important;
|
||||
box-shadow: inset rgba($colorSelectedFg, 0.1) 0 0 0 1px;
|
||||
box-shadow: inset rgba($colorSelectedFg, 0.8) 0 0 0 1px;
|
||||
color: $colorSelectedFg !important;
|
||||
|
||||
@include styleTliEm($colorSelectedFg);
|
||||
}
|
||||
|
||||
@include styleTliEm($baseFgEm);
|
||||
@ -308,6 +324,7 @@
|
||||
|
||||
&__progress {
|
||||
fill: $colorInProgressFgEm;
|
||||
transform: translateX(50%) translateY(50%);
|
||||
}
|
||||
|
||||
&__sweep-hand {
|
||||
|
@ -99,7 +99,10 @@ export default class ViewLargeAction {
|
||||
}
|
||||
);
|
||||
this.preview = vNode.componentInstance;
|
||||
this.destroy = destroy;
|
||||
this.destroy = () => {
|
||||
destroy();
|
||||
this.preview = null;
|
||||
};
|
||||
|
||||
return this.preview.$el;
|
||||
}
|
||||
|
@ -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() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user