Compare commits
26 Commits
hex-values
...
mct7583
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4849bcc04 | ||
|
|
5f0bd10c61 | ||
|
|
8c2558bfe0 | ||
|
|
0eadc7a4ae | ||
|
|
cad4652a08 | ||
|
|
8379f2d073 | ||
|
|
8ed112a4a8 | ||
|
|
14c58c4410 | ||
|
|
87ba9fcbc0 | ||
|
|
ab49f3f3a1 | ||
|
|
df969722d1 | ||
|
|
ef62633df1 | ||
|
|
597dc58eb7 | ||
|
|
5d00d642f3 | ||
|
|
39ab81c3d0 | ||
|
|
0bdd0963a4 | ||
|
|
eae51356c8 | ||
|
|
a36ad3f5e7 | ||
|
|
307ededd19 | ||
|
|
d7ecfdf10f | ||
|
|
a1c36f314d | ||
|
|
86e636cbce | ||
|
|
0d2b36ae82 | ||
|
|
95072da257 | ||
|
|
faa2621e26 | ||
|
|
73eead6b72 |
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 a notable change that will require a special callout in the release notes [Notable Change](../docs/src/process/release.md) ? For example, will this break compatibility with existing APIs or projects which source these plugins?
|
||||
* [ ] 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
|
||||
|
||||
|
||||
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:
|
||||
|
||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@@ -5,7 +5,6 @@
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"rvest.vs-code-prettier-eslint"
|
||||
],
|
||||
|
||||
@@ -15,5 +15,5 @@ export default merge(common, {
|
||||
__OPENMCT_ROOT_RELATIVE__: '""'
|
||||
})
|
||||
],
|
||||
devtool: 'source-map'
|
||||
devtool: 'eval-source-map'
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -447,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')`
|
||||
|
||||
@@ -505,15 +505,14 @@ async function setTimeConductorBounds(page, startDate, endDate) {
|
||||
* @param {string} startDate
|
||||
* @param {string} endDate
|
||||
*/
|
||||
async function setIndependentTimeConductorBounds(page, startDate, endDate) {
|
||||
// Activate Independent Time Conductor in Fixed Time Mode
|
||||
await page.getByRole('switch').click();
|
||||
async function setIndependentTimeConductorBounds(page, { start, end }) {
|
||||
// Activate Independent Time Conductor
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Bring up the time conductor popup
|
||||
await page.click('.c-conductor-holder--compact .c-compact-tc');
|
||||
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||
await expect(page.locator('.itc-popout')).toBeInViewport();
|
||||
|
||||
await setTimeBounds(page, startDate, endDate);
|
||||
await setTimeBounds(page, start, end);
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import { devices } from '@playwright/test';
|
||||
const MAX_FAILURES = 5;
|
||||
const NUM_WORKERS = 2;
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -20,7 +19,8 @@ const config = {
|
||||
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
|
||||
},
|
||||
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
|
||||
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
|
||||
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,
|
||||
|
||||
26
e2e/test-data/display_layout_with_child_overlay_plot.json
Normal file
26
e2e/test-data/display_layout_with_child_overlay_plot.json
Normal file
File diff suppressed because one or more lines are too long
@@ -174,6 +174,6 @@ test.describe('AppActions', () => {
|
||||
type: 'Folder'
|
||||
});
|
||||
await openObjectTreeContextMenu(page, folder.url);
|
||||
await expect(page.getByLabel('Menu')).toBeVisible();
|
||||
await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,13 +33,18 @@
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
setIndependentTimeConductorBounds,
|
||||
setTimeConductorBounds
|
||||
} from '../../appActions.js';
|
||||
import { MISSION_TIME } from '../../constants.js';
|
||||
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,
|
||||
@@ -89,6 +94,53 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Generate display layout with 1 child overlay plot', async ({ page, context }) => {
|
||||
const parent = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Parent Display Layout'
|
||||
});
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Child Overlay Plot 1',
|
||||
parent: parent.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Child SWG 1',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(parent.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: '2024-11-12 19:11:11.000Z',
|
||||
end: '2024-11-12 20:11:11.000Z'
|
||||
});
|
||||
|
||||
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
|
||||
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
|
||||
|
||||
await setTimeConductorBounds(page, NEW_GLOBAL_START_BOUNDS, NEW_GLOBAL_END_BOUNDS);
|
||||
|
||||
// Verify that the global time conductor bounds have been updated
|
||||
expect(
|
||||
await page.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_START_BOUNDS);
|
||||
expect(
|
||||
await page.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_END_BOUNDS);
|
||||
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({
|
||||
path: fileURLToPath(
|
||||
new URL(
|
||||
'../../../e2e/test-data/display_layout_with_child_overlay_plot.json',
|
||||
import.meta.url
|
||||
)
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => {
|
||||
// Create Display Layout
|
||||
const parent = await createDomainObjectWithDefaults(page, {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -131,7 +131,10 @@ test.describe('Time Strip', () => {
|
||||
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
|
||||
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
|
||||
|
||||
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: startBoundString,
|
||||
end: endBoundString
|
||||
});
|
||||
expect(await activityBounds.count()).toEqual(1);
|
||||
});
|
||||
|
||||
@@ -160,7 +163,10 @@ test.describe('Time Strip', () => {
|
||||
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
|
||||
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
|
||||
|
||||
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: startBoundString,
|
||||
end: endBoundString
|
||||
});
|
||||
|
||||
// Verify that two events are displayed
|
||||
expect(await activityBounds.count()).toEqual(2);
|
||||
|
||||
@@ -286,7 +286,7 @@ test.describe('Basic Condition Set Use', () => {
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await page.click('button[title="Change the current view"]');
|
||||
await page.getByLabel('Open the View Switcher Menu').click();
|
||||
|
||||
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
|
||||
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
|
||||
|
||||
@@ -23,6 +23,7 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
setFixedTimeMode,
|
||||
setIndependentTimeConductorBounds,
|
||||
setRealTimeMode,
|
||||
@@ -30,12 +31,120 @@ import {
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
const LOCALSTORAGE_PATH = fileURLToPath(
|
||||
const CHILD_LAYOUT_STORAGE_STATE_PATH = fileURLToPath(
|
||||
new URL('../../../../test-data/display_layout_with_child_layouts.json', import.meta.url)
|
||||
);
|
||||
const CHILD_PLOT_STORAGE_STATE_PATH = fileURLToPath(
|
||||
new URL('../../../../test-data/display_layout_with_child_overlay_plot.json', import.meta.url)
|
||||
);
|
||||
const TINY_IMAGE_BASE64 =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
|
||||
|
||||
test.describe('Display Layout Sub-object Actions @localStorage', () => {
|
||||
const INIT_ITC_START_BOUNDS = '2024-11-12 19:11:11.000Z';
|
||||
const INIT_ITC_END_BOUNDS = '2024-11-12 20:11:11.000Z';
|
||||
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
|
||||
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
|
||||
|
||||
test.use({
|
||||
storageState: CHILD_PLOT_STORAGE_STATE_PATH
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
const waitForMyItemsNavigation = page.waitForURL(`**/mine/?*`);
|
||||
await page
|
||||
.getByLabel('Main Tree')
|
||||
.getByLabel('Navigate to Parent Display Layout layout Object')
|
||||
.click();
|
||||
// Wait for the URL to change to the display layout
|
||||
await waitForMyItemsNavigation;
|
||||
});
|
||||
test('Open in New Tab action preserves time bounds @2p', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7524'
|
||||
});
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6982'
|
||||
});
|
||||
|
||||
const TEST_FIXED_START_TIME = 1731352271000; // 2024-11-11 19:11:11.000Z
|
||||
const TEST_FIXED_END_TIME = TEST_FIXED_START_TIME + 3600000; // 2024-11-11 20:11:11.000Z
|
||||
|
||||
// Verify the ITC has the expected initial bounds
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('Start bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_START_BOUNDS);
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('End bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_END_BOUNDS);
|
||||
|
||||
// Update the global fixed bounds to 2024-11-11 19:11:11.000Z / 2024-11-11 20:11:11.000Z
|
||||
const url = page.url().split('?')[0];
|
||||
await navigateToObjectWithFixedTimeBounds(
|
||||
page,
|
||||
url,
|
||||
TEST_FIXED_START_TIME,
|
||||
TEST_FIXED_END_TIME
|
||||
);
|
||||
|
||||
// ITC bounds should still match the initial ITC bounds
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('Start bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_START_BOUNDS);
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('End bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_END_BOUNDS);
|
||||
|
||||
// Open the Child Overlay Plot 1 in a new tab
|
||||
await page.getByLabel('View menu items').click();
|
||||
const pagePromise = page.context().waitForEvent('page');
|
||||
await page.getByLabel('Open In New Tab').click();
|
||||
|
||||
const newPage = await pagePromise;
|
||||
await newPage.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Verify that the global time conductor bounds in the new page match the updated global bounds
|
||||
expect(
|
||||
await newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_START_BOUNDS);
|
||||
expect(
|
||||
await newPage.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_END_BOUNDS);
|
||||
|
||||
// Verify that the ITC is enabled in the new page
|
||||
await expect(newPage.getByLabel('Disable Independent Time Conductor')).toBeVisible();
|
||||
// Verify that the ITC bounds in the new page match the original ITC bounds
|
||||
expect(
|
||||
await newPage
|
||||
.getByLabel('Independent Time Conductor Panel')
|
||||
.getByLabel('Start bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_START_BOUNDS);
|
||||
expect(
|
||||
await newPage
|
||||
.getByLabel('Independent Time Conductor Panel')
|
||||
.getByLabel('End bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_END_BOUNDS);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Display Layout Toolbar Actions @localStorage', () => {
|
||||
const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';
|
||||
const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1';
|
||||
@@ -50,7 +159,7 @@ test.describe('Display Layout Toolbar Actions @localStorage', () => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
});
|
||||
test.use({
|
||||
storageState: LOCALSTORAGE_PATH
|
||||
storageState: CHILD_LAYOUT_STORAGE_STATE_PATH
|
||||
});
|
||||
|
||||
test('can add/remove Text element to a single layout', async ({ page }) => {
|
||||
@@ -163,7 +272,7 @@ test.describe('Display Layout', () => {
|
||||
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
|
||||
// ensure we can right click on the alpha-numeric widget and view historical data
|
||||
await page.getByLabel('Sine', { exact: true }).click({
|
||||
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByLabel('View Historical Data').click();
|
||||
@@ -336,7 +445,7 @@ test.describe('Display Layout', () => {
|
||||
|
||||
const startDate = '2021-12-30 01:01:00.000Z';
|
||||
const endDate = '2021-12-30 01:11:00.000Z';
|
||||
await setIndependentTimeConductorBounds(page, startDate, endDate);
|
||||
await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,11 +248,10 @@ test.describe('Flexible Layout', () => {
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// flip on independent time conductor
|
||||
await setIndependentTimeConductorBounds(
|
||||
page,
|
||||
'2021-12-30 01:01:00.000Z',
|
||||
'2021-12-30 01:11:00.000Z'
|
||||
);
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: '2021-12-30 01:01:00.000Z',
|
||||
end: '2021-12-30 01:11:00.000Z'
|
||||
});
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
||||
@@ -175,13 +175,13 @@ test.describe('Gauge', () => {
|
||||
});
|
||||
|
||||
// Try to create a Folder into the Gauge. Should be disallowed.
|
||||
await page.getByRole('button', { name: /Create/ }).click();
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.getByRole('menuitem', { name: /Folder/ }).click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.getByLabel('Cancel').click();
|
||||
|
||||
// Try to create a Display Layout into the Gauge. Should be disallowed.
|
||||
await page.getByRole('button', { name: /Create/ }).click();
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.getByRole('menuitem', { name: /Display Layout/ }).click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
});
|
||||
|
||||
@@ -37,6 +37,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
});
|
||||
|
||||
test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => {
|
||||
const initStartBounds = await page.getByLabel('Start bounds').textContent();
|
||||
const initEndBounds = await page.getByLabel('End bounds').textContent();
|
||||
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Data Visualization Source'
|
||||
});
|
||||
@@ -78,5 +80,9 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
await newPage.waitForLoadState();
|
||||
// expect new tab title to contain 'Second Sine Wave Generator'
|
||||
await expect(newPage).toHaveTitle('Second Sine Wave Generator');
|
||||
|
||||
// Verify that "Open in New Tab" preserves the time bounds
|
||||
expect(initStartBounds).toEqual(await newPage.getByLabel('Start bounds').textContent());
|
||||
expect(initEndBounds).toEqual(await newPage.getByLabel('End bounds').textContent());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ test.describe('Snapshot Container tests', () => {
|
||||
// name: "Dropped Overlay Plot"
|
||||
// });
|
||||
|
||||
await page.getByLabel('Take a Notebook Snapshot').click();
|
||||
await page.getByLabel('Open the Notebook Snapshot Menu').click();
|
||||
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
|
||||
await page.getByLabel('Show Snapshots').click();
|
||||
});
|
||||
|
||||
@@ -189,6 +189,57 @@ test.describe('Overlay Plot', () => {
|
||||
await assertLimitLinesExistAndAreVisible(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
|
||||
}) => {
|
||||
@@ -305,6 +356,10 @@ test.describe('Overlay Plot', () => {
|
||||
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
|
||||
@@ -319,6 +374,23 @@ test.describe('Overlay Plot', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -337,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;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,9 @@ test.describe('Plots work in Previews', () => {
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// right click on the plot and select view large
|
||||
await page.getByLabel('Sine', { exact: true }).click({ button: 'right' });
|
||||
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByLabel('View Historical Data').click();
|
||||
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
|
||||
@@ -114,7 +114,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultFrameBorderColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337
|
||||
@@ -122,7 +124,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultFrameBorderColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -143,7 +147,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@@ -151,7 +157,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
|
||||
@@ -160,7 +168,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot1
|
||||
@@ -168,7 +176,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@@ -176,7 +186,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Save Flexible Layout
|
||||
@@ -191,7 +203,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@@ -199,7 +213,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -241,7 +257,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2 to verify they are the default
|
||||
@@ -249,7 +267,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Set styles using setStyles function on StackedPlot2
|
||||
@@ -258,7 +278,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot2 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@@ -266,7 +286,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Save Flexible Layout
|
||||
@@ -281,7 +303,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@@ -289,7 +313,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Directly navigate to the flexible layout
|
||||
@@ -326,7 +352,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2 matches previous set colors
|
||||
@@ -334,7 +362,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -356,7 +386,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
// Check styles using checkStyles function
|
||||
@@ -364,7 +394,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Save Flexible Layout
|
||||
@@ -386,7 +418,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
'No Style',
|
||||
'No Style',
|
||||
'No Style',
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
// Check styles using checkStyles function
|
||||
@@ -394,7 +426,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(inheritedColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
// Save Flexible Layout
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
@@ -408,7 +442,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(inheritedColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ test.describe('Style Inspector Options', () => {
|
||||
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
|
||||
|
||||
// Select Stacked Layout Column
|
||||
await page.getByLabel('Stacked Plot Frame').click();
|
||||
await page.getByRole('group', { name: 'Stacked Plot Frame' }).click();
|
||||
|
||||
// The overall Flex Layout or Stacked Plot itself MUST be style-able.
|
||||
await expect(page.getByRole('tab', { name: 'Styles' })).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 }) => {
|
||||
|
||||
@@ -109,7 +109,7 @@ test.describe('Verify tooltips', () => {
|
||||
|
||||
async function getToolTip(object) {
|
||||
await page.locator('.c-create-button').hover();
|
||||
await page.getByRole('cell', { name: object.name }).hover();
|
||||
await page.getByLabel('lad name').getByText(object.name).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
||||
@@ -35,25 +35,61 @@ Make no assumptions about the order that elements appear in the DOM.
|
||||
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
test('Verify that My Items Tree appears @mobile', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
//Go to baseURL
|
||||
await page.goto('./');
|
||||
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('./');
|
||||
});
|
||||
|
||||
//My Items to be visible
|
||||
await expect(page.getByRole('treeitem', { name: `${myItemsFolderName}` })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Verify that user can search @mobile', async ({ page }) => {
|
||||
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
|
||||
await page.goto('./');
|
||||
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
|
||||
//Search Results appear in search modal
|
||||
await expect(page.getByLabel('Object Results').getByText('Parent Display Layout')).toBeVisible();
|
||||
//Clicking on the search result takes you to the object
|
||||
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
|
||||
await page.getByTitle('Collapse Browse Pane').click();
|
||||
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,7 +178,7 @@ test.describe('Performance tests', () => {
|
||||
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
|
||||
|
||||
// Click Close Icon
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
await page.evaluate(() => window.performance.mark('view-large-close-button'));
|
||||
|
||||
//await client.send('HeapProfiler.enable');
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -68,14 +69,34 @@ test.describe('Visual - Header @a11y', () => {
|
||||
});
|
||||
|
||||
test('show snapshot button', async ({ page, theme }) => {
|
||||
await page.getByLabel('Take a Notebook Snapshot').click();
|
||||
await page.getByLabel('Open the Notebook Snapshot Menu').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
|
||||
|
||||
await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {
|
||||
scope: header
|
||||
});
|
||||
await expect(await page.getByLabel('Show Snapshots')).toBeVisible();
|
||||
await expect(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
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
|
||||
@@ -91,7 +91,7 @@ test.describe('Flexible Layout styling @a11y', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
await percySnapshot(
|
||||
|
||||
@@ -53,11 +53,11 @@ test.describe('Visual - Telemetry Views', () => {
|
||||
await page.goto(telemetry.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Click this button to see telemetry display options
|
||||
await page.getByRole('button', { name: 'Plot' }).click();
|
||||
await page.getByLabel('Open the View Switcher Menu').click();
|
||||
await page.getByLabel('Telemetry Table').click();
|
||||
|
||||
//Get Table View in place
|
||||
expect(await page.getByLabel('Expand Columns')).toBeInViewport();
|
||||
await expect(page.getByLabel('Expand Columns')).toBeInViewport();
|
||||
|
||||
await percySnapshot(page, `Default Telemetry Table View (theme: ${theme})`);
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
@@ -77,6 +78,10 @@ export default {
|
||||
selectItem(item, event) {
|
||||
event.stopPropagation();
|
||||
const bounds = this.openmct.time.getBounds();
|
||||
const otherBounds = {
|
||||
start: bounds.start - ONE_HOUR,
|
||||
end: bounds.end + ONE_HOUR
|
||||
};
|
||||
const selection = [
|
||||
{
|
||||
element: this.$el,
|
||||
@@ -88,6 +93,9 @@ export default {
|
||||
icon: item.type.cssClass
|
||||
},
|
||||
dataRanges: [
|
||||
{
|
||||
bounds: otherBounds
|
||||
},
|
||||
{
|
||||
bounds
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
32
package.json
32
package.json
@@ -5,13 +5,14 @@
|
||||
"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",
|
||||
@@ -22,10 +23,11 @@
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"codecov": "3.8.3",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"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,14 +38,14 @@
|
||||
"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",
|
||||
"flatbush": "4.2.0",
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "4.0.1",
|
||||
"imports-loader": "5.0.0",
|
||||
"jasmine-core": "5.1.1",
|
||||
"karma": "6.4.2",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
@@ -54,26 +56,26 @@
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.4.0",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"karma-webpack": "5.0.0",
|
||||
"karma-webpack": "5.0.1",
|
||||
"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",
|
||||
"moment-timezone": "0.5.41",
|
||||
"npm-run-all2": "6.1.1",
|
||||
"npm-run-all2": "6.1.2",
|
||||
"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",
|
||||
"sass": "1.68.0",
|
||||
"sass-loader": "14.0.0",
|
||||
"sanitize-html": "2.12.1",
|
||||
"sass": "1.71.1",
|
||||
"sass-loader": "14.1.1",
|
||||
"sinon": "17.0.0",
|
||||
"style-loader": "3.3.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
@@ -81,11 +83,11 @@
|
||||
"typescript": "5.3.3",
|
||||
"uuid": "9.0.1",
|
||||
"vue": "3.4.19",
|
||||
"vue-eslint-parser": "9.3.2",
|
||||
"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": {
|
||||
|
||||
@@ -29,10 +29,13 @@
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:aria-disabled="action.isDisabled"
|
||||
:class="action.cssClass"
|
||||
:aria-label="action.name"
|
||||
aria-describedby="item-description"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItem(action)"
|
||||
@mouseleave="toggleItem()"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
@@ -52,16 +55,23 @@
|
||||
v-for="action in options.actions"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
aria-describedby="item-description"
|
||||
:aria-disabled="action.isDisabled"
|
||||
:class="action.cssClass"
|
||||
:aria-label="action.name"
|
||||
:title="action.description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItem(action)"
|
||||
@mouseleave="toggleItem()"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
<li v-if="options.actions.length === 0">No actions defined.</li>
|
||||
</ul>
|
||||
<div v-if="hoveredItem" id="item-description" class="visually-hidden" aria-live="polite">
|
||||
<span v-if="hoveredItem.name">{{ hoveredItem.name }}</span>
|
||||
<span v-if="hoveredItem.description">: {{ hoveredItem.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -70,11 +80,21 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
|
||||
export default {
|
||||
mixins: [popupMenuMixin],
|
||||
inject: ['options'],
|
||||
data() {
|
||||
return {
|
||||
hoveredItem: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
optionsLabel() {
|
||||
const label = this.options.label ? `${this.options.label} Menu` : 'Menu';
|
||||
const label = this.options.label ? `${this.options.label} Context Menu` : 'Context Menu';
|
||||
return label;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItem(action) {
|
||||
this.hoveredItem = action ?? null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:aria-disabled="action.isDisabled"
|
||||
aria-describedby="item-description"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
@@ -64,7 +64,7 @@
|
||||
role="menuitem"
|
||||
:class="action.cssClass"
|
||||
:aria-label="action.name"
|
||||
:title="action.description"
|
||||
aria-describedby="item-description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
@@ -74,13 +74,13 @@
|
||||
<li v-if="options.actions.length === 0">No actions defined.</li>
|
||||
</ul>
|
||||
|
||||
<div class="c-super-menu__item-description">
|
||||
<div :class="['l-item-description__icon', 'bg-' + hoveredItem.cssClass]"></div>
|
||||
<div aria-live="polite" class="c-super-menu__item-description">
|
||||
<div :class="itemDescriptionIconClass"></div>
|
||||
<div class="l-item-description__name">
|
||||
{{ hoveredItem.name }}
|
||||
{{ hoveredItemName }}
|
||||
</div>
|
||||
<div class="l-item-description__description">
|
||||
{{ hoveredItem.description }}
|
||||
<div id="item-description" class="l-item-description__description">
|
||||
{{ hoveredItemDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,26 +90,39 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
|
||||
export default {
|
||||
mixins: [popupMenuMixin],
|
||||
inject: ['options'],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
hoveredItem: {}
|
||||
hoveredItem: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
optionsLabel() {
|
||||
const label = this.options.label ? `${this.options.label} Super Menu` : 'Super Menu';
|
||||
return label;
|
||||
},
|
||||
itemDescriptionIconClass() {
|
||||
const iconClass = ['l-item-description__icon'];
|
||||
if (this.hoveredItem) {
|
||||
iconClass.push('bg-' + this.hoveredItem.cssClass);
|
||||
}
|
||||
return iconClass;
|
||||
},
|
||||
hoveredItemName() {
|
||||
return this.hoveredItem?.name ?? '';
|
||||
},
|
||||
hoveredItemDescription() {
|
||||
return this.hoveredItem?.description ?? '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItemDescription(action = {}) {
|
||||
toggleItemDescription(action = null) {
|
||||
const hoveredItem = {
|
||||
name: action.name,
|
||||
description: action.description,
|
||||
cssClass: action.cssClass
|
||||
name: action?.name,
|
||||
description: action?.description,
|
||||
cssClass: action?.cssClass
|
||||
};
|
||||
|
||||
this.hoveredItem = Object.assign({}, this.hoveredItem, hoveredItem);
|
||||
this.hoveredItem = hoveredItem;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import installWorker from './WebSocketWorker.js';
|
||||
const DEFAULT_RATE_MS = 1000;
|
||||
/**
|
||||
* Describes the strategy to be used when batching WebSocket messages
|
||||
*
|
||||
@@ -51,11 +50,21 @@ const DEFAULT_RATE_MS = 1000;
|
||||
*
|
||||
* @memberof module:openmct.telemetry
|
||||
*/
|
||||
// Shim for Internet Explorer, I mean Safari. It doesn't support requestIdleCallback, but it's in a tech preview, so it will be dropping soon.
|
||||
const requestIdleCallback =
|
||||
// eslint-disable-next-line compat/compat
|
||||
window.requestIdleCallback ?? ((fn, { timeout }) => setTimeout(fn, timeout));
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECONDS = 5 * ONE_SECOND;
|
||||
|
||||
class BatchingWebSocket extends EventTarget {
|
||||
#worker;
|
||||
#openmct;
|
||||
#showingRateLimitNotification;
|
||||
#rate;
|
||||
#maxBatchSize;
|
||||
#applicationIsInitializing;
|
||||
#maxBatchWait;
|
||||
#firstBatchReceived;
|
||||
|
||||
constructor(openmct) {
|
||||
super();
|
||||
@@ -66,7 +75,10 @@ class BatchingWebSocket extends EventTarget {
|
||||
this.#worker = new Worker(workerUrl);
|
||||
this.#openmct = openmct;
|
||||
this.#showingRateLimitNotification = false;
|
||||
this.#rate = DEFAULT_RATE_MS;
|
||||
this.#maxBatchSize = Number.POSITIVE_INFINITY;
|
||||
this.#maxBatchWait = ONE_SECOND;
|
||||
this.#applicationIsInitializing = true;
|
||||
this.#firstBatchReceived = false;
|
||||
|
||||
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
|
||||
this.#worker.addEventListener('message', routeMessageToHandler);
|
||||
@@ -78,6 +90,20 @@ class BatchingWebSocket extends EventTarget {
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
openmct.once('start', () => {
|
||||
// An idle callback is a pretty good indication that a complex display is done loading. At that point set the batch size more conservatively.
|
||||
// Force it after 5 seconds if it hasn't happened yet.
|
||||
requestIdleCallback(
|
||||
() => {
|
||||
this.#applicationIsInitializing = false;
|
||||
this.setMaxBatchSize(this.#maxBatchSize);
|
||||
},
|
||||
{
|
||||
timeout: FIVE_SECONDS
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,14 +155,6 @@ class BatchingWebSocket extends EventTarget {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When using batching, sets the rate at which batches of messages are released.
|
||||
* @param {Number} rate the amount of time to wait, in ms, between batches.
|
||||
*/
|
||||
setRate(rate) {
|
||||
this.#rate = rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} maxBatchSize the maximum length of a batch of messages. For example,
|
||||
* the maximum number of telemetry values to batch before dropping them
|
||||
@@ -151,12 +169,29 @@ class BatchingWebSocket extends EventTarget {
|
||||
* 15 would probably be a better batch size.
|
||||
*/
|
||||
setMaxBatchSize(maxBatchSize) {
|
||||
this.#maxBatchSize = maxBatchSize;
|
||||
if (!this.#applicationIsInitializing) {
|
||||
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
|
||||
}
|
||||
}
|
||||
setMaxBatchWait(wait) {
|
||||
this.#maxBatchWait = wait;
|
||||
this.#sendBatchWaitToWorker(this.#maxBatchWait);
|
||||
}
|
||||
#sendMaxBatchSizeToWorker(maxBatchSize) {
|
||||
this.#worker.postMessage({
|
||||
type: 'setMaxBatchSize',
|
||||
maxBatchSize
|
||||
});
|
||||
}
|
||||
|
||||
#sendBatchWaitToWorker(maxBatchWait) {
|
||||
this.#worker.postMessage({
|
||||
type: 'setMaxBatchWait',
|
||||
maxBatchWait
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the associated WebSocket. Generally speaking there is no need to call
|
||||
* this manually.
|
||||
@@ -169,7 +204,9 @@ class BatchingWebSocket extends EventTarget {
|
||||
|
||||
#routeMessageToHandler(message) {
|
||||
if (message.data.type === 'batch') {
|
||||
if (message.data.batch.dropped === true && !this.#showingRateLimitNotification) {
|
||||
this.start = Date.now();
|
||||
const batch = message.data.batch;
|
||||
if (batch.dropped === true && !this.#showingRateLimitNotification) {
|
||||
const notification = this.#openmct.notifications.alert(
|
||||
'Telemetry dropped due to client rate limiting.',
|
||||
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
|
||||
@@ -179,16 +216,45 @@ class BatchingWebSocket extends EventTarget {
|
||||
this.#showingRateLimitNotification = false;
|
||||
});
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('batch', { detail: message.data.batch }));
|
||||
setTimeout(() => {
|
||||
this.#readyForNextBatch();
|
||||
}, this.#rate);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('batch', { detail: batch }));
|
||||
this.#waitUntilIdleAndRequestNextBatch(batch);
|
||||
} else if (message.data.type === 'message') {
|
||||
this.dispatchEvent(new CustomEvent('message', { detail: message.data.message }));
|
||||
} else if (message.data.type === 'reconnected') {
|
||||
this.dispatchEvent(new CustomEvent('reconnected'));
|
||||
} else {
|
||||
throw new Error(`Unknown message type: ${message.data.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
#waitUntilIdleAndRequestNextBatch(batch) {
|
||||
requestIdleCallback(
|
||||
(state) => {
|
||||
if (this.#firstBatchReceived === false) {
|
||||
this.#firstBatchReceived = true;
|
||||
}
|
||||
const now = Date.now();
|
||||
const waitedFor = now - this.start;
|
||||
if (state.didTimeout === true) {
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.warn(`Event loop is too busy to process batch.`);
|
||||
this.#waitUntilIdleAndRequestNextBatch(batch);
|
||||
} else {
|
||||
// After ingesting a telemetry batch, wait until the event loop is idle again before
|
||||
// informing the worker we are ready for another batch.
|
||||
this.#readyForNextBatch();
|
||||
}
|
||||
} else {
|
||||
if (waitedFor > ONE_SECOND) {
|
||||
console.warn(`Warning, batch processing took ${waitedFor}ms`);
|
||||
}
|
||||
this.#readyForNextBatch();
|
||||
}
|
||||
},
|
||||
{ timeout: ONE_SECOND }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BatchingWebSocket;
|
||||
|
||||
@@ -85,6 +85,7 @@ const SUBSCRIBE_STRATEGY = {
|
||||
export default class TelemetryAPI {
|
||||
#isGreedyLAD;
|
||||
#subscribeCache;
|
||||
#hasReturnedFirstData;
|
||||
|
||||
get SUBSCRIBE_STRATEGY() {
|
||||
return SUBSCRIBE_STRATEGY;
|
||||
@@ -108,6 +109,7 @@ export default class TelemetryAPI {
|
||||
this.#isGreedyLAD = true;
|
||||
this.BatchingWebSocket = BatchingWebSocket;
|
||||
this.#subscribeCache = {};
|
||||
this.#hasReturnedFirstData = false;
|
||||
}
|
||||
|
||||
abortAllRequests() {
|
||||
@@ -383,7 +385,10 @@ export default class TelemetryAPI {
|
||||
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
|
||||
try {
|
||||
const telemetry = await provider.request(...arguments);
|
||||
|
||||
if (!this.#hasReturnedFirstData) {
|
||||
this.#hasReturnedFirstData = true;
|
||||
performance.mark('firstHistoricalDataReturned');
|
||||
}
|
||||
return telemetry;
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
/* eslint-disable max-classes-per-file */
|
||||
export default function installWorker() {
|
||||
const ONE_SECOND = 1000;
|
||||
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
|
||||
|
||||
/**
|
||||
@@ -44,6 +45,13 @@ export default function installWorker() {
|
||||
#currentWaitIndex = 0;
|
||||
#messageCallbacks = [];
|
||||
#wsUrl;
|
||||
#reconnecting = false;
|
||||
#worker;
|
||||
|
||||
constructor(worker) {
|
||||
super();
|
||||
this.#worker = worker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a new WebSocket connection to the given URL
|
||||
@@ -62,6 +70,9 @@ export default function installWorker() {
|
||||
this.#isConnecting = true;
|
||||
|
||||
this.#webSocket = new WebSocket(url);
|
||||
//Exposed to e2e tests so that the websocket can be manipulated during tests. Cannot find any other way to do this.
|
||||
// Playwright does not support forcing websocket state changes.
|
||||
this.#worker.currentWebSocket = this.#webSocket;
|
||||
|
||||
const boundConnected = this.#connected.bind(this);
|
||||
this.#webSocket.addEventListener('open', boundConnected);
|
||||
@@ -100,12 +111,17 @@ export default function installWorker() {
|
||||
}
|
||||
|
||||
#connected() {
|
||||
console.debug('Websocket connected.');
|
||||
console.info('Websocket connected.');
|
||||
this.#isConnected = true;
|
||||
this.#isConnecting = false;
|
||||
this.#currentWaitIndex = 0;
|
||||
|
||||
this.dispatchEvent(new Event('connected'));
|
||||
if (this.#reconnecting) {
|
||||
this.#worker.postMessage({
|
||||
type: 'reconnected'
|
||||
});
|
||||
this.#reconnecting = false;
|
||||
}
|
||||
|
||||
this.#flushQueue();
|
||||
}
|
||||
@@ -138,6 +154,7 @@ export default function installWorker() {
|
||||
if (this.#reconnectTimeoutHandle) {
|
||||
return;
|
||||
}
|
||||
this.#reconnecting = true;
|
||||
|
||||
this.#reconnectTimeoutHandle = setTimeout(() => {
|
||||
this.connect(this.#wsUrl);
|
||||
@@ -207,6 +224,9 @@ export default function installWorker() {
|
||||
case 'setMaxBatchSize':
|
||||
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
|
||||
break;
|
||||
case 'setMaxBatchWait':
|
||||
this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown message type: ${type}`);
|
||||
}
|
||||
@@ -245,7 +265,6 @@ export default function installWorker() {
|
||||
}
|
||||
|
||||
routeMessageToHandler(data) {
|
||||
//Implement batching here
|
||||
if (this.#messageBatcher.shouldBatchMessage(data)) {
|
||||
this.#messageBatcher.addMessageToBatch(data);
|
||||
} else {
|
||||
@@ -267,12 +286,15 @@ export default function installWorker() {
|
||||
#maxBatchSize;
|
||||
#readyForNextBatch;
|
||||
#worker;
|
||||
#throttledSendNextBatch;
|
||||
|
||||
constructor(worker) {
|
||||
this.#maxBatchSize = 10;
|
||||
// No dropping telemetry unless we're explicitly told to.
|
||||
this.#maxBatchSize = Number.POSITIVE_INFINITY;
|
||||
this.#readyForNextBatch = false;
|
||||
this.#worker = worker;
|
||||
this.#resetBatch();
|
||||
this.setMaxBatchWait(ONE_SECOND);
|
||||
}
|
||||
#resetBatch() {
|
||||
this.#batch = {};
|
||||
@@ -310,23 +332,29 @@ export default function installWorker() {
|
||||
const batchId = this.#batchingStrategy.getBatchIdFromMessage(message);
|
||||
let batch = this.#batch[batchId];
|
||||
if (batch === undefined) {
|
||||
this.#hasBatch = true;
|
||||
batch = this.#batch[batchId] = [message];
|
||||
} else {
|
||||
batch.push(message);
|
||||
}
|
||||
if (batch.length > this.#maxBatchSize) {
|
||||
console.warn(
|
||||
`Exceeded max batch size of ${this.#maxBatchSize} for ${batchId}. Dropping value.`
|
||||
);
|
||||
batch.shift();
|
||||
this.#batch.dropped = this.#batch.dropped || true;
|
||||
this.#batch.dropped = true;
|
||||
}
|
||||
|
||||
if (this.#readyForNextBatch) {
|
||||
this.#sendNextBatch();
|
||||
} else {
|
||||
this.#hasBatch = true;
|
||||
this.#throttledSendNextBatch();
|
||||
}
|
||||
}
|
||||
setMaxBatchSize(maxBatchSize) {
|
||||
this.#maxBatchSize = maxBatchSize;
|
||||
}
|
||||
setMaxBatchWait(maxBatchWait) {
|
||||
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), maxBatchWait);
|
||||
}
|
||||
/**
|
||||
* Indicates that client code is ready to receive the next batch of
|
||||
* messages. If a batch is available, it will be immediately sent.
|
||||
@@ -335,7 +363,7 @@ export default function installWorker() {
|
||||
*/
|
||||
readyForNextBatch() {
|
||||
if (this.#hasBatch) {
|
||||
this.#sendNextBatch();
|
||||
this.#throttledSendNextBatch();
|
||||
} else {
|
||||
this.#readyForNextBatch = true;
|
||||
}
|
||||
@@ -352,7 +380,34 @@ export default function installWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
const websocket = new ResilientWebSocket();
|
||||
function throttle(callback, wait) {
|
||||
let last = 0;
|
||||
let throttling = false;
|
||||
|
||||
return function (...args) {
|
||||
if (throttling) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
const timeSinceLast = now - last;
|
||||
|
||||
if (timeSinceLast >= wait) {
|
||||
last = now;
|
||||
callback(...args);
|
||||
} else if (!throttling) {
|
||||
throttling = true;
|
||||
|
||||
setTimeout(() => {
|
||||
last = performance.now();
|
||||
throttling = false;
|
||||
callback(...args);
|
||||
}, wait - timeSinceLast);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const websocket = new ResilientWebSocket(self);
|
||||
const messageBatcher = new MessageBatcher(self);
|
||||
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
|
||||
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
|
||||
@@ -363,4 +418,6 @@ export default function installWorker() {
|
||||
websocket.registerMessageCallback((data) => {
|
||||
websocketBroker.routeMessageToHandler(data);
|
||||
});
|
||||
|
||||
self.websocketInstance = websocket;
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@
|
||||
<tr
|
||||
ref="tableRow"
|
||||
class="js-lad-table__body__row c-table__selectable-row"
|
||||
aria-label="lad row"
|
||||
@click="clickedRow"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
>
|
||||
<td
|
||||
ref="tableCell"
|
||||
aria-label="lad name"
|
||||
class="js-first-data"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
@@ -58,7 +60,7 @@
|
||||
const CONTEXT_MENU_ACTIONS = ['viewDatumAction', 'viewHistoricalData', 'remove'];
|
||||
const BLANK_VALUE = '---';
|
||||
|
||||
import identifierToString from '/src/tools/url.js';
|
||||
import { objectPathToUrl } from '/src/tools/url.js';
|
||||
import PreviewAction from '@/ui/preview/PreviewAction.js';
|
||||
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
|
||||
@@ -260,7 +262,7 @@ export default {
|
||||
event.preventDefault();
|
||||
this.preview(this.objectPath);
|
||||
} else {
|
||||
const resultUrl = identifierToString(this.openmct, this.objectPath);
|
||||
const resultUrl = objectPathToUrl(this.openmct, this.objectPath);
|
||||
this.openmct.router.navigate(resultUrl);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<template>
|
||||
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver" :class="staleClass">
|
||||
<table class="c-table c-lad-table" :class="applyLayoutClass">
|
||||
<table aria-label="lad table" class="c-table c-lad-table" :class="applyLayoutClass">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
aria-label="Clock Indicator"
|
||||
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
|
||||
role="complementary"
|
||||
aria-live="off"
|
||||
>
|
||||
<span class="label c-indicator__label">
|
||||
{{ timeTextValue }}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
aria-label="sub object frame"
|
||||
class="l-layout__frame c-frame"
|
||||
:class="{
|
||||
'no-frame': !item.hasFrame,
|
||||
|
||||
@@ -37,24 +37,24 @@
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
aria-label="Alpha-numeric telemetry"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<div
|
||||
class="is-status__indicator"
|
||||
:aria-label="`This item is ${status}`"
|
||||
:title="`This item is ${status}`"
|
||||
></div>
|
||||
<div class="is-status__indicator"></div>
|
||||
<div v-if="showLabel" class="c-telemetry-view__label">
|
||||
<div class="c-telemetry-view__label-text">
|
||||
<div
|
||||
class="c-telemetry-view__label-text"
|
||||
:aria-label="`Alpha-numeric telemetry name for ${domainObject.name}`"
|
||||
>
|
||||
{{ domainObject.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showValue"
|
||||
:aria-label="fieldName"
|
||||
:aria-label="`Alpha-numeric telemetry value of ${telemetryValue}`"
|
||||
:title="fieldName"
|
||||
class="c-telemetry-view__value"
|
||||
:class="[telemetryClass]"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -220,6 +220,7 @@
|
||||
lengthAdjust="spacing"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
:aria-label="`gauge value of ${curVal}`"
|
||||
x="50%"
|
||||
y="50%"
|
||||
>
|
||||
|
||||
@@ -25,7 +25,18 @@
|
||||
<div class="c-inspect-properties">
|
||||
<div class="c-inspect-properties__header">Numeric Data</div>
|
||||
</div>
|
||||
<div ref="numericDataView"></div>
|
||||
<div ref="numericDataView">
|
||||
<TelemetryFrame
|
||||
v-for="plotObject of plotObjects"
|
||||
:key="plotObject.identifier.key"
|
||||
:bounds="bounds"
|
||||
:telemetry-object="plotObject"
|
||||
:path="[plotObject]"
|
||||
:render-when-visible="plotObject.renderWhenVisible"
|
||||
>
|
||||
<Plot />
|
||||
</TelemetryFrame>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasNumericData">
|
||||
{{ noNumericDataText }}
|
||||
@@ -33,13 +44,15 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';
|
||||
import Plot from '../plot/PlotView.vue';
|
||||
import TelemetryFrame from './TelemetryFrame.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TelemetryFrame,
|
||||
Plot
|
||||
},
|
||||
inject: ['openmct', 'domainObject', 'timeFormatter'],
|
||||
props: {
|
||||
bounds: {
|
||||
@@ -90,16 +103,19 @@ export default {
|
||||
this.clearPlots();
|
||||
|
||||
this.unregisterTimeContextList = [];
|
||||
this.componentsList = [];
|
||||
this.elementsList = [];
|
||||
this.visibilityObservers = [];
|
||||
|
||||
this.telemetryKeys.forEach(async (telemetryKey) => {
|
||||
const plotObject = await this.openmct.objects.get(telemetryKey);
|
||||
const visibilityObserver = new VisibilityObserver(
|
||||
this.$refs.numericDataView,
|
||||
this.openmct.element
|
||||
);
|
||||
plotObject.renderWhenVisible = visibilityObserver.renderWhenVisible;
|
||||
|
||||
this.visibilityObservers.push(visibilityObserver);
|
||||
this.plotObjects.push(plotObject);
|
||||
this.unregisterTimeContextList.push(this.setIndependentTimeContextForComponent(plotObject));
|
||||
this.renderPlot(plotObject);
|
||||
});
|
||||
},
|
||||
setIndependentTimeContextForComponent(plotObject) {
|
||||
@@ -110,63 +126,14 @@ export default {
|
||||
// set the time context of the object to the selected time range
|
||||
return this.openmct.time.addIndependentContext(keyString, this.bounds);
|
||||
},
|
||||
renderPlot(plotObject) {
|
||||
const wrapper = document.createElement('div');
|
||||
const visibilityObserver = new VisibilityObserver(wrapper, this.openmct.element);
|
||||
|
||||
const { destroy } = mount(
|
||||
{
|
||||
components: {
|
||||
TelemetryFrame,
|
||||
Plot
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
path: [plotObject],
|
||||
renderWhenVisible: visibilityObserver.renderWhenVisible
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
plotObject,
|
||||
bounds: this.bounds
|
||||
};
|
||||
},
|
||||
template: `<TelemetryFrame
|
||||
:bounds="bounds"
|
||||
:telemetry-object="plotObject"
|
||||
>
|
||||
<Plot />
|
||||
</TelemetryFrame>`
|
||||
},
|
||||
{
|
||||
app: this.openmct.app,
|
||||
element: wrapper
|
||||
}
|
||||
);
|
||||
|
||||
this.componentsList.push(destroy);
|
||||
this.elementsList.push(wrapper);
|
||||
this.visibilityObservers.push(visibilityObserver);
|
||||
this.$refs.numericDataView.append(wrapper);
|
||||
},
|
||||
clearPlots() {
|
||||
if (this.componentsList?.length) {
|
||||
this.componentsList.forEach((destroy) => destroy());
|
||||
delete this.componentsList;
|
||||
}
|
||||
|
||||
if (this.elementsList?.length) {
|
||||
this.elementsList.forEach((element) => element.remove());
|
||||
delete this.elementsList;
|
||||
}
|
||||
|
||||
if (this.visibilityObservers?.length) {
|
||||
this.visibilityObservers.forEach((visibilityObserver) => visibilityObserver.destroy());
|
||||
delete this.visibilityObservers;
|
||||
}
|
||||
|
||||
if (this.plotObjects?.length) {
|
||||
this.plotObjects = [];
|
||||
this.plotObjects.splice(0, this.plotObjects.length);
|
||||
}
|
||||
|
||||
if (this.unregisterTimeContextList?.length) {
|
||||
|
||||
@@ -70,7 +70,9 @@ export default {
|
||||
inject: ['openmct'],
|
||||
provide() {
|
||||
return {
|
||||
domainObject: this.telemetryObject
|
||||
domainObject: this.telemetryObject,
|
||||
path: this.path,
|
||||
renderWhenVisible: this.renderWhenVisible
|
||||
};
|
||||
},
|
||||
props: {
|
||||
@@ -81,6 +83,14 @@ export default {
|
||||
telemetryObject: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
path: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
renderWhenVisible: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -110,7 +120,10 @@ export default {
|
||||
'tc.mode': 'fixed'
|
||||
};
|
||||
const newTabAction = this.openmct.actions.getAction('newTab');
|
||||
newTabAction.invoke([sourceTelemObject], urlParams);
|
||||
// No view context needed, so pass undefined.
|
||||
// The urlParams arg will override the global time bounds with the data visualization
|
||||
// plot bounds.
|
||||
newTabAction.invoke([sourceTelemObject], undefined, urlParams);
|
||||
this.showMenu = false;
|
||||
},
|
||||
previewTelemetry() {
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
import Moment from 'moment';
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import objectPathToUrl from '@/tools/url';
|
||||
import { objectPathToUrl } from '@/tools/url';
|
||||
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
|
||||
import ImageExporter from '../../../exporters/ImageExporter.js';
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
|
||||
<button
|
||||
class="c-icon-button c-button--menu icon-camera"
|
||||
aria-label="Take a Notebook Snapshot"
|
||||
title="Take a Notebook Snapshot"
|
||||
:aria-label="snapshotMenuLabel"
|
||||
:title="snapshotMenuLabel"
|
||||
@click.stop.prevent="showMenu"
|
||||
>
|
||||
<span title="Take Notebook Snapshot" class="c-icon-button__label"> Snapshot </span>
|
||||
<span class="c-icon-button__label">Snapshot</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -72,6 +72,11 @@ export default {
|
||||
notebookTypes: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
snapshotMenuLabel() {
|
||||
return 'Open the Notebook Snapshot Menu';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
validateNotebookStorageObject();
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import objectPathToUrl from '/src/tools/url.js';
|
||||
import { objectPathToUrl } from '/src/tools/url.js';
|
||||
export default class OpenInNewTab {
|
||||
constructor(openmct) {
|
||||
this.name = 'Open In New Tab';
|
||||
@@ -31,8 +31,26 @@ export default class OpenInNewTab {
|
||||
|
||||
this._openmct = openmct;
|
||||
}
|
||||
invoke(objectPath, urlParams = undefined) {
|
||||
let url = objectPathToUrl(this._openmct, objectPath, urlParams);
|
||||
window.open(url);
|
||||
|
||||
/**
|
||||
* Invokes the "Open in New Tab" action. This will open the object in a new
|
||||
* browser tab. The URL for the new tab is determined by the current object
|
||||
* path and any custom time bounds.
|
||||
*
|
||||
* @param {import('@/api/objects/ObjectAPI').DomainObject[]} objectPath The current object path
|
||||
* @param {ViewContext} _view The view context for the object being opened (unused)
|
||||
* @param {Object<string, string | number>} customUrlParams Provides the ability to override
|
||||
* the global time conductor bounds. It is an object with the following key/value pairs:
|
||||
* ```
|
||||
* {
|
||||
* 'tc.start': <number>,
|
||||
* 'tc.end': <number>,
|
||||
* 'tc.mode': 'fixed' | 'local' | <string>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
invoke(objectPath, _view, customUrlParams) {
|
||||
const url = objectPathToUrl(this._openmct, objectPath, customUrlParams);
|
||||
window.open(url, undefined, 'noopener');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import { toRaw } from 'vue';
|
||||
import { onMounted, ref, toRaw } from 'vue';
|
||||
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
|
||||
@@ -295,7 +295,7 @@ import CSVExporter from '../../../exporters/CSVExporter.js';
|
||||
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
||||
import Search from '../../../ui/components/SearchComponent.vue';
|
||||
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
|
||||
import throttle from '../../../utils/throttle';
|
||||
import { useResizeObserver } from '../../../ui/composables/resize.js';
|
||||
import SizingRow from './SizingRow.vue';
|
||||
import TableColumnHeader from './TableColumnHeader.vue';
|
||||
import TableFooterIndicator from './TableFooterIndicator.vue';
|
||||
@@ -303,7 +303,6 @@ import TelemetryTableRow from './TableRow.vue';
|
||||
|
||||
const VISIBLE_ROW_COUNT = 100;
|
||||
const ROW_HEIGHT = 17;
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const AUTO_SCROLL_TRIGGER_HEIGHT = ROW_HEIGHT * 3;
|
||||
|
||||
export default {
|
||||
@@ -354,6 +353,15 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['marked-rows-updated', 'filter'],
|
||||
setup() {
|
||||
const root = ref(null);
|
||||
const { size: containerSize, startObserving } = useResizeObserver();
|
||||
onMounted(() => {
|
||||
startObserving(root.value);
|
||||
});
|
||||
|
||||
return { containerSize, root };
|
||||
},
|
||||
data() {
|
||||
let configuration = this.table.configuration.getConfiguration();
|
||||
|
||||
@@ -441,6 +449,13 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
//This should be refactored so that it doesn't require an explicit watch. Should be doable.
|
||||
containerSize: {
|
||||
handler() {
|
||||
this.debouncedRescaleToContainer();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
loading: {
|
||||
handler(isLoading) {
|
||||
if (isLoading) {
|
||||
@@ -500,9 +515,10 @@ export default {
|
||||
this.filterTelemetry = _.debounce(this.filterTelemetry, 500);
|
||||
},
|
||||
mounted() {
|
||||
this.throttledUpdateVisibleRows = _.throttle(this.updateVisibleRows, 1000, { leading: true });
|
||||
this.debouncedRescaleToContainer = _.debounce(this.rescaleToContainer, 300);
|
||||
|
||||
this.csvExporter = new CSVExporter();
|
||||
this.rowsAdded = _.throttle(this.rowsAdded, 200);
|
||||
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
|
||||
this.scroll = _.throttle(this.scroll, 100);
|
||||
|
||||
if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {
|
||||
@@ -515,8 +531,6 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
this.updateVisibleRows = throttle(this.updateVisibleRows, 1000);
|
||||
|
||||
this.table.on('object-added', this.addObject);
|
||||
this.table.on('object-removed', this.removeObject);
|
||||
this.table.on('refresh', this.clearRowsAndRerender);
|
||||
@@ -526,8 +540,8 @@ export default {
|
||||
|
||||
this.table.tableRows.on('add', this.rowsAdded);
|
||||
this.table.tableRows.on('remove', this.rowsRemoved);
|
||||
this.table.tableRows.on('sort', this.updateVisibleRows);
|
||||
this.table.tableRows.on('filter', this.updateVisibleRows);
|
||||
this.table.tableRows.on('sort', this.throttledUpdateVisibleRows);
|
||||
this.table.tableRows.on('filter', this.throttledUpdateVisibleRows);
|
||||
|
||||
this.openmct.time.on('bounds', this.boundsChanged);
|
||||
|
||||
@@ -540,10 +554,10 @@ export default {
|
||||
this.table.configuration.on('change', this.updateConfiguration);
|
||||
|
||||
this.calculateTableSize();
|
||||
this.pollForResize();
|
||||
this.calculateScrollbarWidth();
|
||||
|
||||
this.table.initialize();
|
||||
this.rescaleToContainer();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.table.off('object-added', this.addObject);
|
||||
@@ -555,15 +569,13 @@ export default {
|
||||
|
||||
this.table.tableRows.off('add', this.rowsAdded);
|
||||
this.table.tableRows.off('remove', this.rowsRemoved);
|
||||
this.table.tableRows.off('sort', this.updateVisibleRows);
|
||||
this.table.tableRows.off('filter', this.updateVisibleRows);
|
||||
this.table.tableRows.off('sort', this.throttledUpdateVisibleRows);
|
||||
this.table.tableRows.off('filter', this.throttledUpdateVisibleRows);
|
||||
|
||||
this.table.configuration.off('change', this.updateConfiguration);
|
||||
|
||||
this.openmct.time.off('bounds', this.boundsChanged);
|
||||
|
||||
clearInterval(this.resizePollHandle);
|
||||
|
||||
this.table.configuration.destroy();
|
||||
|
||||
this.table.destroy();
|
||||
@@ -684,7 +696,7 @@ export default {
|
||||
this.table.sortBy(this.sortOptions);
|
||||
},
|
||||
scroll() {
|
||||
this.updateVisibleRows();
|
||||
this.throttledUpdateVisibleRows();
|
||||
this.synchronizeScrollX();
|
||||
|
||||
if (this.shouldAutoScroll()) {
|
||||
@@ -757,11 +769,11 @@ export default {
|
||||
this.initiateAutoScroll();
|
||||
}
|
||||
|
||||
this.updateVisibleRows();
|
||||
this.throttledUpdateVisibleRows();
|
||||
},
|
||||
rowsRemoved(rows) {
|
||||
this.setHeight();
|
||||
this.updateVisibleRows();
|
||||
this.throttledUpdateVisibleRows();
|
||||
},
|
||||
/**
|
||||
* Calculates height based on total number of rows, and sets table height.
|
||||
@@ -880,35 +892,27 @@ export default {
|
||||
dropTargetActive(isActive) {
|
||||
this.isDropTargetActive = isActive;
|
||||
},
|
||||
pollForResize() {
|
||||
let el = this.$refs.root;
|
||||
let width = el.clientWidth;
|
||||
let height = el.clientHeight;
|
||||
rescaleToContainer() {
|
||||
let scrollTop = this.scrollable.scrollTop;
|
||||
|
||||
this.resizePollHandle = setInterval(() => {
|
||||
this.renderWhenVisible(() => {
|
||||
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
|
||||
this.calculateTableSize();
|
||||
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
|
||||
// Need to preserve scroll position in this case.
|
||||
if (this.autoScroll) {
|
||||
this.initiateAutoScroll();
|
||||
} else {
|
||||
this.scrollable.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
width = el.clientWidth;
|
||||
height = el.clientHeight;
|
||||
this.renderWhenVisible(() => {
|
||||
if (this.isAutosizeEnabled) {
|
||||
this.calculateTableSize();
|
||||
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
|
||||
// Need to preserve scroll position in this case.
|
||||
if (this.autoScroll) {
|
||||
this.initiateAutoScroll();
|
||||
} else {
|
||||
this.scrollable.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
scrollTop = this.scrollable.scrollTop;
|
||||
});
|
||||
}, RESIZE_POLL_INTERVAL);
|
||||
scrollTop = this.scrollable.scrollTop;
|
||||
});
|
||||
},
|
||||
clearRowsAndRerender() {
|
||||
this.visibleRows = [];
|
||||
this.$nextTick().then(this.updateVisibleRows);
|
||||
this.$nextTick().then(this.throttledUpdateVisibleRows);
|
||||
},
|
||||
pause(byButton) {
|
||||
if (byButton) {
|
||||
|
||||
@@ -35,7 +35,6 @@ import utcMultiTimeFormat from './utcMultiTimeFormat.js';
|
||||
|
||||
const PADDING = 1;
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const PIXELS_PER_TICK = 100;
|
||||
const PIXELS_PER_TICK_WIDE = 200;
|
||||
|
||||
@@ -92,7 +91,6 @@ export default {
|
||||
|
||||
//Respond to changes in conductor
|
||||
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearInterval(this.resizeTimer);
|
||||
|
||||
@@ -148,7 +148,10 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
|
||||
this.handleNewBounds = _.throttle(this.handleNewBounds, 300, {
|
||||
leading: true,
|
||||
trailing: false
|
||||
});
|
||||
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
|
||||
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
|
||||
this.setTimeContext();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode',
|
||||
{ 'is-expanded': independentTCEnabled }
|
||||
]"
|
||||
aria-label="Independent Time Conductor Panel"
|
||||
>
|
||||
<ToggleSwitch
|
||||
id="independentTCToggle"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
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 {
|
||||
|
||||
@@ -68,6 +68,18 @@ div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
// Provides a way to add accessible text to elements
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/******************************************************** BROWSER ELEMENTS */
|
||||
body.desktop {
|
||||
::-webkit-scrollbar {
|
||||
@@ -361,7 +373,7 @@ body.desktop .has-local-controls {
|
||||
}
|
||||
}
|
||||
|
||||
[aria-disabled = 'true'],
|
||||
[aria-disabled='true'],
|
||||
*[disabled],
|
||||
.disabled {
|
||||
opacity: $controlDisabledOpacity;
|
||||
@@ -397,7 +409,7 @@ body.desktop .has-local-controls {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
content: '';
|
||||
right: 0;
|
||||
width: $fadeTruncateW * 1.5;
|
||||
z-index: 2;
|
||||
|
||||
@@ -24,10 +24,17 @@
|
||||
* Module defining url handling.
|
||||
*/
|
||||
|
||||
function getUrlParams(openmct, customUrlParams = {}) {
|
||||
/**
|
||||
* Convert the current URL parameters to an array of strings.
|
||||
* @param {import('../../openmct').OpenMCT} openmct
|
||||
* @returns {Array<string>} newTabParams
|
||||
*/
|
||||
export function paramsToArray(openmct, customUrlParams = {}) {
|
||||
let urlParams = openmct.router.getParams();
|
||||
Object.entries(customUrlParams).forEach((urlParam) => {
|
||||
const [key, value] = urlParam;
|
||||
|
||||
// Merge the custom URL parameters with the current URL parameters.
|
||||
Object.entries(customUrlParams).forEach((param) => {
|
||||
const [key, value] = param;
|
||||
urlParams[key] = value;
|
||||
});
|
||||
|
||||
@@ -39,21 +46,7 @@ function getUrlParams(openmct, customUrlParams = {}) {
|
||||
delete urlParams['tc.endBound'];
|
||||
}
|
||||
|
||||
return urlParams;
|
||||
}
|
||||
|
||||
export function paramsToArray(openmct, customUrlParams = {}) {
|
||||
// parse urlParams from an object to an array.
|
||||
let urlParams = getUrlParams(openmct, customUrlParams);
|
||||
let newTabParams = [];
|
||||
for (let key in urlParams) {
|
||||
if ({}.hasOwnProperty.call(urlParams, key)) {
|
||||
let param = `${key}=${urlParams[key]}`;
|
||||
newTabParams.push(param);
|
||||
}
|
||||
}
|
||||
|
||||
return newTabParams;
|
||||
return Object.entries(urlParams).map(([key, value]) => `${key}=${value}`);
|
||||
}
|
||||
|
||||
export function identifierToString(openmct, objectPath) {
|
||||
@@ -66,7 +59,7 @@ export function identifierToString(openmct, objectPath) {
|
||||
* @param {any} customUrlParams
|
||||
* @returns {string} url
|
||||
*/
|
||||
export default function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
|
||||
export function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
|
||||
let url = identifierToString(openmct, objectPath);
|
||||
|
||||
let urlParams = paramsToArray(openmct, customUrlParams);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createOpenMct, resetApplicationState } from '../utils/testing.js';
|
||||
import { default as objectPathToUrl, identifierToString, paramsToArray } from './url.js';
|
||||
import { identifierToString, objectPathToUrl, paramsToArray } from './url.js';
|
||||
|
||||
describe('the url tool', function () {
|
||||
let openmct;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
'has-complex-content': complexContent
|
||||
}
|
||||
]"
|
||||
:aria-label="ariaLabel"
|
||||
>
|
||||
<div class="c-so-view__header">
|
||||
<div class="c-object-label" :class="[statusClass]">
|
||||
@@ -45,6 +46,8 @@
|
||||
<div
|
||||
ref="objectName"
|
||||
class="c-object-label__name"
|
||||
aria-label="object name"
|
||||
:title="domainObject && domainObject.name"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
@@ -58,6 +61,7 @@
|
||||
'c-so-view__frame-controls--no-frame': !hasFrame,
|
||||
'has-complex-content': complexContent
|
||||
}"
|
||||
:aria-label="`${ariaLabel} Controls`"
|
||||
>
|
||||
<div v-if="supportsIndependentTime" class="c-conductor-holder--compact">
|
||||
<independent-time-conductor :domain-object="domainObject" :object-path="objectPath" />
|
||||
@@ -163,6 +167,9 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ariaLabel() {
|
||||
return `${this.domainObject.name} Frame`;
|
||||
},
|
||||
statusClass() {
|
||||
return this.status ? `is-status--${this.status}` : '';
|
||||
}
|
||||
|
||||
@@ -105,7 +105,9 @@ export default {
|
||||
return this.status ? `is-status--${this.status}` : '';
|
||||
},
|
||||
ariaLabel() {
|
||||
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${this.domainObject.type} Object`;
|
||||
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${
|
||||
this.domainObject.type
|
||||
} Object`;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -29,12 +29,14 @@
|
||||
import { axisTop } from 'd3-axis';
|
||||
import { scaleLinear, scaleUtc } from 'd3-scale';
|
||||
import { select } from 'd3-selection';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import utcMultiTimeFormat from '@/plugins/timeConductor/utcMultiTimeFormat';
|
||||
|
||||
import { useResizeObserver } from '../composables/resize';
|
||||
|
||||
//TODO: UI direction needed for the following property values
|
||||
const PADDING = 1;
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const PIXELS_PER_TICK = 100;
|
||||
const PIXELS_PER_TICK_WIDE = 200;
|
||||
//This offset needs to be re-considered
|
||||
@@ -73,6 +75,16 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const axisHolder = ref(null);
|
||||
const { size, startObserving } = useResizeObserver();
|
||||
onMounted(() => {
|
||||
startObserving(axisHolder.value);
|
||||
});
|
||||
return {
|
||||
containerSize: size
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
bounds(newBounds) {
|
||||
this.drawAxis(newBounds, this.timeSystem);
|
||||
@@ -82,6 +94,9 @@ export default {
|
||||
},
|
||||
contentHeight() {
|
||||
this.updateNowMarker();
|
||||
},
|
||||
containerSize() {
|
||||
this.resize();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -100,7 +115,7 @@ export default {
|
||||
|
||||
this.setDimensions();
|
||||
this.drawAxis(this.bounds, this.timeSystem);
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
this.resize();
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval(this.resizeTimer);
|
||||
|
||||
@@ -145,7 +145,11 @@
|
||||
:show-edit-view="true"
|
||||
@change-action-collection="setActionCollection"
|
||||
/>
|
||||
<component :is="conductorComponent" class="l-shell__time-conductor" />
|
||||
<component
|
||||
:is="conductorComponent"
|
||||
class="l-shell__time-conductor"
|
||||
aria-label="Global Time Conductor"
|
||||
/>
|
||||
</pane>
|
||||
<pane
|
||||
class="l-shell__pane-inspector l-pane--holds-multipane"
|
||||
|
||||
@@ -24,9 +24,10 @@
|
||||
<button
|
||||
class="c-create-button c-button--menu c-button--major icon-plus"
|
||||
:aria-disabled="isEditing"
|
||||
aria-labelledby="create-button-label"
|
||||
@click.prevent.stop="showCreateMenu"
|
||||
>
|
||||
<span class="c-button__label">Create</span>
|
||||
<span id="create-button-label" class="c-button__label">Create</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
<button
|
||||
class="c-icon-button c-button--menu"
|
||||
:class="currentView.cssClass"
|
||||
title="Change the current view"
|
||||
:title="viewSwitcherLabel"
|
||||
:aria-label="viewSwitcherLabel"
|
||||
@click.prevent.stop="showMenu"
|
||||
>
|
||||
<span class="c-icon-button__label">
|
||||
@@ -51,6 +52,11 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['set-view'],
|
||||
computed: {
|
||||
viewSwitcherLabel() {
|
||||
return 'Open the View Switcher Menu';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setView(view) {
|
||||
this.$emit('set-view', view);
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
<script>
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
|
||||
import identifierToString from '../../../tools/url.js';
|
||||
import { objectPathToUrl } from '../../../tools/url.js';
|
||||
import ObjectPath from '../../components/ObjectPath.vue';
|
||||
import PreviewAction from '../../preview/PreviewAction.js';
|
||||
|
||||
@@ -101,7 +101,7 @@ export default {
|
||||
event.preventDefault();
|
||||
this.preview(objectPath);
|
||||
} else {
|
||||
let resultUrl = identifierToString(this.openmct, objectPath);
|
||||
let resultUrl = objectPathToUrl(this.openmct, objectPath);
|
||||
|
||||
// Remove the vestigial 'ROOT' identifier from url if it exists
|
||||
if (resultUrl.includes('/ROOT')) {
|
||||
|
||||
@@ -61,7 +61,8 @@ export default {
|
||||
let sortedActions = this.openmct.actions._groupAndSortActions(actions);
|
||||
|
||||
const menuOptions = {
|
||||
onDestroy: this.onContextMenuDestroyed
|
||||
onDestroy: this.onContextMenuDestroyed,
|
||||
label: this.objectPath[0].name
|
||||
};
|
||||
|
||||
const menuItems = this.openmct.menus.actionsToMenuItems(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import objectPathToUrl from '../../tools/url.js';
|
||||
import { objectPathToUrl } from '../../tools/url.js';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
|
||||
Reference in New Issue
Block a user