Compare commits

..

2 Commits

Author SHA1 Message Date
e9358c0552 Merge branch 'master' into vue-3 2022-11-03 12:15:10 -07:00
6414a6f556 WIP 2022-11-03 12:09:43 -07:00
102 changed files with 638 additions and 1416 deletions

View File

@ -13,18 +13,14 @@ updates:
- "pr:daveit"
- "pr:platform"
ignore:
#We have to source the playwright container which is not detected by Dependabot
- dependency-name: "@playwright/test"
- dependency-name: "playwright-core"
#Lots of noise in these type patch releases.
- dependency-name: "@babel/eslint-parser"
- dependency-name: "@playwright/test" #We have to source the playwright container which is not detected by Dependabot
- dependency-name: "playwright-core" #We have to source the playwright container which is not detected by Dependabot
- dependency-name: "@babel/eslint-parser" #Lots of noise in these type patch releases.
update-types: ["version-update:semver-patch"]
- dependency-name: "eslint-plugin-vue"
- dependency-name: "eslint-plugin-vue" #Lots of noise in these type patch releases.
update-types: ["version-update:semver-patch"]
- dependency-name: "babel-loader"
update-types: ["version-update:semver-patch"]
- dependency-name: "sinon"
update-types: ["version-update:semver-patch"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:

View File

@ -16,11 +16,7 @@ jobs:
with:
node-version: 16
- run: npm install
- run: |
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
npm whoami
npm publish --access=public --tag unstable openmct
# - run: npm test
- run: npm test
publish-npm-prerelease:
needs: build
@ -32,6 +28,6 @@ jobs:
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm install
- run: npm publish --access=public --tag unstable
- run: npm publish --access public --tag unstable
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -21,10 +21,4 @@
!copyright-notice.html
!index.html
!openmct.js
!SECURITY.md
# Add e2e tests to npm package
!/e2e/**/*
# ... except our test-data folder files.
/e2e/test-data/*.json
!SECURITY.md

View File

@ -10,7 +10,7 @@ accept changes from external contributors.
The short version:
1. Write your contribution or describe your idea in the form of a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [start a GitHub discussion](https://github.com/nasa/openmct/discussions).
1. Write your contribution or describe your idea in the form of an [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions)
2. Make sure your contribution meets code, test, and commit message
standards as described below.
3. Submit a pull request from a topic branch back to `master`. Include a check

View File

@ -6,8 +6,10 @@ Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting S
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
![Screen Shot 2022-11-23 at 9 51 36 AM](https://user-images.githubusercontent.com/4215777/203617422-4d912bfc-766f-4074-8324-409d9bbe7c05.png)
## See Open MCT in Action
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).
![Demo](https://nasa.github.io/openmct/static/res/images/Open-MCT.Browse.Layout.Mars-Weather-1.jpg)
## Building and Running Open MCT Locally

View File

@ -46,7 +46,6 @@
*/
const Buffer = require('buffer').Buffer;
const genUuid = require('uuid').v4;
/**
* This common function creates a domain object with the default options. It is the preferred way of creating objects
@ -57,10 +56,6 @@ const genUuid = require('uuid').v4;
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
if (!name) {
name = `${type}:${genUuid()}`;
}
const parentUrl = await getHashUrlToDomainObject(page, parent);
// Navigate to the parent object. This is necessary to create the object
@ -72,18 +67,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
await page.click('button:has-text("Create")');
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("${type}")`);
await page.click(`li:text("${type}")`);
// Modify the name input field of the domain object to accept 'name'
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill("");
await nameInput.fill(name);
if (page.testNotes) {
// Fill the "Notes" section with information about the
// currently running test and its project.
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
await notesInput.fill(page.testNotes);
if (name) {
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill("");
await nameInput.fill(name);
}
// Click OK button and wait for Navigate event
@ -106,8 +96,8 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
}
return {
name,
uuid,
name: name || `Unnamed ${type}`,
uuid: uuid,
url: objectUrl
};
}

View File

@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { createDomainObjectWithDefaults } = require('../appActions');
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
/**
@ -40,17 +38,24 @@ async function enterTextEntry(page, text) {
/**
* @param {import('@playwright/test').Page} page
*/
async function dragAndDropEmbed(page, notebookObject) {
// Create example telemetry object
const swg = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator"
});
// Navigate to notebook
await page.goto(notebookObject.url);
// Expand the tree to reveal the notebook
await page.click('button[title="Show selected item in tree"]');
// Drag and drop the SWG into the notebook
await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA);
async function dragAndDropEmbed(page, myItemsFolderName) {
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Sine Wave Generator")
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click form[name="mctForm"] >> text=My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
// Click text=OK
await page.locator('text=OK').click();
// Click text=Open MCT My Items >> span >> nth=3
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
// Click text=Unnamed CUSTOM_NAME
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed CUSTOM_NAME').click()
]);
await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
}
// eslint-disable-next-line no-undef

View File

@ -126,21 +126,13 @@ exports.test = test.extend({
// This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js
theme: [theme, { option: true }],
// eslint-disable-next-line no-shadow
page: async ({ page, theme }, use, testInfo) => {
page: async ({ page, theme }, use) => {
// eslint-disable-next-line playwright/no-conditional-in-test
if (theme === 'snow') {
//inject snow theme
await page.addInitScript({ path: path.join(__dirname, './helper', './useSnowTheme.js') });
}
// Attach info about the currently running test and its project.
// This will be used by appActions to fill in the created
// domain object's notes.
page.testNotes = [
`${testInfo.titlePath.join('\n')}`,
`${testInfo.project.name}`
].join('\n');
await use(page);
},
myItemsFolderName: [myItemsFolderName, { option: true }],
@ -148,5 +140,22 @@ exports.test = test.extend({
openmctConfig: async ({ myItemsFolderName }, use) => {
await use({ myItemsFolderName });
}
// objectCreateOptions: [objectCreateOptions, {option: true}],
// eslint-disable-next-line no-shadow
// domainObject: [async ({ page, objectCreateOptions }, use) => {
// // FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule.
// // eslint-disable-next-line playwright/no-conditional-in-test
// if (objectCreateOptions === null) {
// await use(page);
// return;
// }
// //Go to baseURL
// await page.goto('./', { waitUntil: 'networkidle' });
// const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
// await use({ uuid });
// }, { auto: true }]
});
exports.expect = expect;

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../pluginFixtures.js');
const { test, expect } = require('../../baseFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js');
test.describe('AppActions', () => {
@ -50,11 +50,11 @@ test.describe('AppActions', () => {
});
await page.goto(timer1.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Foo');
await page.goto(timer2.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Bar');
await page.goto(timer3.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
});
await test.step('Create multiple nested objects in a row', async () => {
@ -74,11 +74,11 @@ test.describe('AppActions', () => {
parent: folder2.uuid
});
await page.goto(folder1.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Foo');
await page.goto(folder2.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Bar');
await page.goto(folder3.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Baz');
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);

View File

@ -45,7 +45,7 @@
*/
// Structure: Some standard Imports. Please update the required pathing.
const { test, expect } = require('../../pluginFixtures');
const { test, expect } = require('../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
/**
@ -144,5 +144,5 @@ async function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) {
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer);
// Click Ok button to Save
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
}

View File

@ -43,14 +43,14 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
//Add a 5000 ms Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@ -58,7 +58,7 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
// focus the overlay plot
await page.goto(overlayPlot.url);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
//Save localStorage for future test execution
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
});

View File

@ -25,7 +25,7 @@
*
*/
const { test, expect } = require('../../pluginFixtures');
const { test, expect } = require('../../baseFixtures');
test.describe("CouchDB Status Indicator @couchdb", () => {
test.use({ failOnConsoleError: false });

View File

@ -24,7 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
*/
const { test, expect } = require('../../../pluginFixtures');
const { test, expect } = require('../../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../../appActions');
test.describe('Example Event Generator CRUD Operations', () => {

View File

@ -96,7 +96,7 @@ test.describe('Sine Wave Generator', () => {
//Click text=OK
await Promise.all([
page.waitForNavigation(),
page.click('button:has-text("OK")')
page.click('text=OK')
]);
// Verify that the Sine Wave Generator is displayed and correct

View File

@ -25,7 +25,6 @@ This test suite is dedicated to tests which verify form functionality in isolati
*/
const { test, expect } = require('../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const path = require('path');
const TEST_FOLDER = 'test folder';
@ -44,7 +43,7 @@ test.describe('Form Validation Behavior', () => {
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
//Required Field Form Validation
await expect(page.locator('button:has-text("OK")')).toBeDisabled();
await expect(page.locator('text=OK')).toBeDisabled();
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
//Correct Form Validation for missing title and trigger validation with 'Tab'
@ -53,13 +52,13 @@ test.describe('Form Validation Behavior', () => {
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
//Required Field Form Validation is corrected
await expect(page.locator('button:has-text("OK")')).toBeEnabled();
await expect(page.locator('text=OK')).toBeEnabled();
await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
//Finish Creating Domain Object
await Promise.all([
page.waitForNavigation(),
page.click('button:has-text("OK")')
page.click('text=OK')
]);
//Verify that the Domain Object has been created with the corrected title property
@ -92,44 +91,6 @@ test.describe('Persistence operations @addInit', () => {
});
});
test.describe('Persistence operations @couchdb', () => {
test.use({ failOnConsoleError: false });
test('Editing object properties should generate a single persistence operation', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5616'
});
await page.goto('./', { waitUntil: 'networkidle' });
// Create a new 'Clock' object with default settings
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock'
});
// Count all persistence operations (PUT requests) for this specific object
let putRequestCount = 0;
page.on('request', req => {
if (req.method() === 'PUT' && req.url().endsWith(clock.uuid)) {
putRequestCount += 1;
}
});
// Open the edit form for the clock object
await page.click('button[title="More options"]');
await page.click('li[title="Edit properties of this object."]');
// Modify the display format from default 12hr -> 24hr and click 'Save'
await page.locator('select[aria-label="12 or 24 hour clock"]').selectOption({ value: 'clock24' });
await page.click('button[aria-label="Save"]');
await expect.poll(() => putRequestCount, {
message: 'Verify a single PUT request was made to persist the object',
timeout: 1000
}).toEqual(1);
});
});
test.describe('Form Correctness by Object Type', () => {
test.fixme('Verify correct behavior of number object (SWG)', async ({page}) => {});
test.fixme('Verify correct behavior of number object Timer', async ({page}) => {});

View File

@ -81,7 +81,7 @@ test.describe('Move & link item tests', () => {
await page.locator('li.icon-move').click();
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
// Expect that Child Folder is in My Items, the root folder
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
@ -95,11 +95,11 @@ test.describe('Move & link item tests', () => {
// Create Telemetry Table
let telemetryTable = 'Test Telemetry Table';
await page.locator('button:has-text("Create")').click();
await page.locator('li[role="menuitem"]:has-text("Telemetry Table")').click();
await page.locator('li:has-text("Telemetry Table")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
// Finish editing and save Telemetry Table
await page.locator('.c-button--menu.c-button--major.icon-save').click();
@ -108,7 +108,7 @@ test.describe('Move & link item tests', () => {
// Create New Folder Basic Domain Object
let folder = 'Test Folder';
await page.locator('button:has-text("Create")').click();
await page.locator('li[role="menuitem"]:has-text("Folder")').click();
await page.locator('li:has-text("Folder")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
@ -120,7 +120,7 @@ test.describe('Move & link item tests', () => {
// Continue test regardless of assertion and create it in My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
// Open My Items
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
@ -196,7 +196,7 @@ test.describe('Move & link item tests', () => {
await page.locator('li.icon-link').click();
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
// Expect that Child Folder is in My Items, the root folder
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();

View File

@ -40,11 +40,11 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.click('button:has-text("Create")');
await page.locator('li[role="menuitem"]:has-text("Condition Set")').click();
await page.locator('li:has-text("Condition Set")').click();
await Promise.all([
page.waitForNavigation(),
page.click('button:has-text("OK")')
page.click('text=OK')
]);
//Save localStorage for future test execution
@ -163,9 +163,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Click hamburger button
await page.locator('[title="More options"]').click();
// Click 'Remove' and press OK
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// Click text=Remove
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
//Expect Unnamed Condition Set to be removed in Main View
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();

View File

@ -23,7 +23,7 @@
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
test.describe('Display Layout', () => {
test.describe('Testing Display Layout @unstable', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
@ -55,12 +55,12 @@ test.describe('Display Layout', () => {
// On getting data, check if the value found in the Display Layout is the most recent value
// from the Sine Wave Generator
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
const formattedTelemetryValue = getTelemValuePromise;
const formattedTelemetryValue = await getTelemValuePromise;
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
const displayLayoutValue = await displayLayoutValuePromise.textContent();
const trimmedDisplayValue = displayLayoutValue.trim();
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
// Create a Display Layout
@ -86,12 +86,12 @@ test.describe('Display Layout', () => {
// On getting data, check if the value found in the Display Layout is the most recent value
// from the Sine Wave Generator
const formattedTelemetryValue = getTelemValuePromise;
const formattedTelemetryValue = await getTelemValuePromise;
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
const displayLayoutValue = await displayLayoutValuePromise.textContent();
const trimmedDisplayValue = displayLayoutValue.trim();
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
});
test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({ page }) => {
// Create a Display Layout
@ -116,20 +116,16 @@ test.describe('Display Layout', () => {
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
// delete
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
});
test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
// Create a Display Layout
const displayLayout = await createDomainObjectWithDefaults(page, {
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: "Test Display Layout"
});
@ -148,18 +144,18 @@ test.describe('Display Layout', () => {
// Expand the Display Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Go to the original Sine Wave Generator to navigate away from the Display Layout
await page.goto(sineWaveObject.url);
// Click the original Sine Wave Generator to navigate away from the Display Layout
await page.locator('.c-tree__item .c-tree__item__name:text("Test Sine Wave Generator")').click();
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
// navigate back to the display layout to confirm it has been removed
await page.goto(displayLayout.url);
await page.locator('.c-tree__item .c-tree__item__name:text("Test Display Layout")').click();
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
});
});

View File

@ -23,13 +23,12 @@
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Flexible Layout', () => {
let sineWaveObject;
test.describe('Testing Flexible Layout @unstable', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator"
});
@ -55,81 +54,13 @@ test.describe('Flexible Layout', () => {
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty');
// Check that panes can be dragged while Flexible Layout is in Edit mode
let dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
let dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'true');
// Save Flexible Layout
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Check that panes are not draggable while Flexible Layout is in Browse mode
dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
});
test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ page }) => {
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: "Test Flexible Layout"
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
// Expand the Flexible Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// Verify that the item has been removed from the layout
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
});
test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
// Create a Flexible Layout
const flexibleLayout = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: "Test Flexible Layout"
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
// Expand the Flexible Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Go to the original Sine Wave Generator to navigate away from the Flexible Layout
await page.goto(sineWaveObject.url);
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// navigate back to the display layout to confirm it has been removed
await page.goto(flexibleLayout.url);
// Verify that the item has been removed from the layout
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
});
});

View File

@ -1,124 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite is dedicated to testing the Gauge component.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Gauge', () => {
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Can add and remove telemetry sources @unstable', async ({ page }) => {
// Create the gauge with defaults
const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' });
const editButtonLocator = page.locator('button[title="Edit"]');
const saveButtonLocator = page.locator('button[title="Save"]');
// Create a sine wave generator within the gauge
const swg1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: gauge.uuid
});
// Navigate to the gauge and verify that
// the SWG appears in the elements pool
await page.goto(gauge.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButtonLocator.click();
await page.locator('li[title="Save and Finish Editing"]').click();
// Create another sine wave generator within the gauge
const swg2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: gauge.uuid
});
// Verify that the 'Replace telemetry source' modal appears and accept it
await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Navigate to the gauge and verify that the new SWG
// appears in the elements pool and the old one is gone
await page.goto(gauge.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
await saveButtonLocator.click();
// Right click on the new SWG in the elements pool and delete it
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
button: 'right'
});
await page.locator('li[title="Remove this object from its containing object."]').click();
// Verify that the 'Remove object' confirmation modal appears and accept it
await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Verify that the elements pool shows no elements
await expect(page.locator('text="No contained elements"')).toBeVisible();
});
test('Can create a non-default Gauge', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5356'
});
//Click the Create button
await page.click('button:has-text("Create")');
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("Gauge")`);
// FIXME: We need better selectors for these custom form controls
const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');
await displayCurrentValueSwitch.setChecked(false);
await page.click('button[aria-label="Save"]');
// TODO: Verify changes in the UI
});
test('Can edit a single Gauge-specific property', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5985'
});
// Create the gauge with defaults
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
await page.click('button[title="More options"]');
await page.click('li[role="menuitem"]:has-text("Edit Properties")');
// FIXME: We need better selectors for these custom form controls
const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');
await displayCurrentValueSwitch.setChecked(false);
await page.click('button[aria-label="Save"]');
// TODO: Verify changes in the UI
});
});

View File

@ -40,10 +40,10 @@ test.describe('Example Imagery Object', () => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create a default 'Example Imagery' object
const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.locator(backgroundImageSelector).hover({trial: true});
});
@ -188,7 +188,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
await page.click('text=Example Imagery');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
@ -197,7 +197,7 @@ test.describe('Example Imagery in Display Layout', () => {
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('button:has-text("OK")'),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@ -275,7 +275,7 @@ test.describe('Example Imagery in Flexible layout', () => {
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
await page.click('text=Example Imagery');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
@ -284,7 +284,7 @@ test.describe('Example Imagery in Flexible layout', () => {
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('button:has-text("OK")'),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@ -317,7 +317,7 @@ test.describe('Example Imagery in Tabs View', () => {
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
await page.click('text=Example Imagery');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
@ -326,7 +326,7 @@ test.describe('Example Imagery in Tabs View', () => {
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('button:has-text("OK")'),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@ -26,7 +26,7 @@ This test suite is dedicated to tests which verify the basic operations surround
// FIXME: Remove this eslint exception once tests are implemented
// eslint-disable-next-line no-unused-vars
const { test, expect } = require('../../../../pluginFixtures');
const { test, expect } = require('../../../../baseFixtures');
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
const nbUtils = require('../../../../helper/notebookUtils');

View File

@ -24,7 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { test, expect } = require('../../../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Notebook Tests with CouchDB @couchdb', () => {

View File

@ -36,27 +36,27 @@ test.describe('Restricted Notebook', () => {
});
test('Can be renamed @addInit', async ({ page }) => {
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`${notebook.name}`);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
});
test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
await openObjectTreeContextMenu(page, notebook.url);
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('Remove');
const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`);
const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
// notebook tree object exists
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
// Click Remove Text
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('text=Remove').click();
// Click 'OK' on confirmation window and wait for save banner to appear
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
page.waitForSelector('.c-message-banner__message')
]);
@ -134,7 +134,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
// Click text=Ok
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
page.locator('text=Ok').click()
]);
// deleted page, should no longer exist
@ -145,9 +145,10 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
test.beforeEach(async ({ page }) => {
const notebook = await startAndAddRestrictedNotebookObject(page);
await nbUtils.dragAndDropEmbed(page, notebook);
test.beforeEach(async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await startAndAddRestrictedNotebookObject(page);
await nbUtils.dragAndDropEmbed(page, myItemsFolderName);
});
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {

View File

@ -36,7 +36,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) {
// Create an entry
@ -45,8 +45,6 @@ async function createNotebookAndEntry(page, iterations = 1) {
await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`Entry ${iteration}`);
}
return notebook;
}
/**
@ -55,7 +53,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
* @param {number} [iterations = 1] - the number of entries (and tags) to create
*/
async function createNotebookEntryAndTags(page, iterations = 1) {
const notebook = await createNotebookAndEntry(page, iterations);
await createNotebookAndEntry(page, iterations);
for (let iteration = 0; iteration < iterations; iteration++) {
// Hover and click "Add Tag" button
@ -77,8 +75,6 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
// Select the "Science" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
}
return notebook;
}
test.describe('Tagging in Notebooks @addInit', () => {
@ -177,10 +173,10 @@ test.describe('Tagging in Notebooks @addInit', () => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
await createDomainObjectWithDefaults(page, { type: 'Clock' });
const ITERATIONS = 4;
const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
await createNotebookEntryAndTags(page, ITERATIONS);
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
@ -193,11 +189,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
page.goto('./#/browse/mine?hideTree=false'),
page.click('.c-disclosure-triangle')
]);
// Click Clock
await page.click(`text=${clock.name}`);
// Click Unnamed Clock
await page.click('text="Unnamed Clock"');
// Click Notebook
await page.click(`text=${notebook.name}`);
// Click Unnamed Notebook
await page.click('text="Unnamed Notebook"');
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
@ -211,13 +207,14 @@ test.describe('Tagging in Notebooks @addInit', () => {
page.waitForLoadState('networkidle')
]);
// Click Notebook
await page.click(`text="${notebook.name}"`);
// Click Unnamed Notebook
await page.click('text="Unnamed Notebook"');
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
await expect(page.locator(entryLocator)).toContainText("Science");
await expect(page.locator(entryLocator)).toContainText("Driving");
}
});
});

View File

@ -110,10 +110,10 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
@ -129,10 +129,10 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -88,10 +88,10 @@ async function makeOverlayPlot(page, myItemsFolderName) {
// create overlay plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@ -106,7 +106,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
// create a sinewave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
// set amplitude to 6, offset 4, period 2
@ -123,7 +123,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@ -88,11 +88,11 @@ async function makeStackedPlot(page, myItemsFolderName) {
// create stacked plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
await page.locator('li:has-text("Stacked Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@ -146,11 +146,11 @@ async function saveStackedPlot(page) {
async function createSineWaveGenerator(page) {
//Create sine wave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@ -68,10 +68,10 @@ async function makeOverlayPlot(page) {
// create overlay plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@ -86,13 +86,13 @@ async function makeOverlayPlot(page) {
// create a sinewave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click OK to make generator
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@ -25,8 +25,8 @@
*
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults} = require('../../../../appActions');
const { test, expect } = require('../../../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Plot Integrity Testing @unstable', () => {
let sineWaveGeneratorObject;
@ -40,6 +40,7 @@ test.describe('Plot Integrity Testing @unstable', () => {
test('Plots do not re-request data when a plot is clicked', async ({ page }) => {
//Navigate to Sine Wave Generator
await page.goto(sineWaveGeneratorObject.url);
//Capture the number of plots points and store as const name numberOfPlotPoints
//Click on the plot canvas
await page.locator('canvas').nth(1).click();
//No request was made to get historical data
@ -50,90 +51,4 @@ test.describe('Plot Integrity Testing @unstable', () => {
});
expect(createMineFolderRequests.length).toEqual(0);
});
test('Plot is rendered when infinity values exist', async ({ page }) => {
// Edit Plot
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
//Get pixel data from Canvas
const plotPixelSize = await getCanvasPixelsWithData(page);
expect(plotPixelSize).toBeGreaterThan(0);
});
});
/**
* This function edits a sine wave generator with the default options and enables the infinity values option.
*
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreateObjectInfo} sineWaveGeneratorObject
* @returns {Promise<CreatedObjectInfo>} An object containing information about the edited domain object.
*/
async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) {
await page.goto(sineWaveGeneratorObject.url);
// Edit LAD table
await page.locator('[title="More options"]').click();
await page.locator('[title="Edit properties of this object."]').click();
// Modify the infinity option to true
const infinityInput = page.locator('[aria-label="Include Infinity Values"]');
await infinityInput.click();
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// FIXME: Changes to SWG properties should be reflected on save, but they're not?
// Thus, navigate away and back to the object.
await page.goto('./#/browse/mine');
await page.goto(sineWaveGeneratorObject.url);
await page.locator('c-progress-bar c-telemetry-table__progress-bar').waitFor({
state: 'hidden'
});
// FIXME: The progress bar disappears on series data load, not on plot render,
// so wait for a half a second before evaluating the canvas.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getCanvasPixelsWithData(page) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
await page.evaluate(() => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
let data;
let canvas;
let ctx;
canvas = document.querySelector('canvas');
ctx = canvas.getContext('2d');
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const imageDataValues = Object.values(data);
let plotPixels = [];
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
for (let i = 0; i < imageDataValues.length;) {
if (imageDataValues[i] > 0) {
plotPixels.push({
startIndex: i,
endIndex: i + 3,
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
});
}
i = i + 4;
}
window.getCanvasValue(plotPixels.length);
});
return getTelemValuePromise;
}

View File

@ -1,93 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite is dedicated to testing the Scatter Plot component.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Scatter Plot', () => {
let scatterPlot;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
// Create the Scatter Plot
scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' });
});
test('Can add and remove telemetry sources', async ({ page }) => {
const editButtonLocator = page.locator('button[title="Edit"]');
const saveButtonLocator = page.locator('button[title="Save"]');
// Create a sine wave generator within the scatter plot
const swg1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: scatterPlot.uuid
});
// Navigate to the scatter plot and verify that
// the SWG appears in the elements pool
await page.goto(scatterPlot.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButtonLocator.click();
await page.locator('li[title="Save and Finish Editing"]').click();
// Create another sine wave generator within the scatter plot
const swg2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: scatterPlot.uuid
});
// Verify that the 'Replace telemetry source' modal appears and accept it
await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Navigate to the scatter plot and verify that the new SWG
// appears in the elements pool and the old one is gone
await page.goto(scatterPlot.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
await saveButtonLocator.click();
// Right click on the new SWG in the elements pool and delete it
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
button: 'right'
});
await page.locator('li[title="Remove this object from its containing object."]').click();
// Verify that the 'Remove object' confirmation modal appears and accept it
await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Verify that the elements pool shows no elements
await expect(page.locator('text="No contained elements"')).toBeVisible();
});
});

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { test, expect } = require('../../../../baseFixtures');
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
test.describe('Time conductor operations', () => {

View File

@ -30,7 +30,7 @@ test.describe('Timer', () => {
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
});
test('Can perform actions on the Timer', async ({ page }) => {
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4313'

View File

@ -31,7 +31,7 @@ test.describe('Grand Search', () => {
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
const createdObjects = await createObjectsForSearch(page);
await createObjectsForSearch(page, myItemsFolderName);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
@ -41,8 +41,8 @@ test.describe('Grand Search', () => {
await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`);
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
// Click the Elements pool to dismiss the search menu
await page.locator('.l-pane__label:has-text("Elements")').click();
// Click text=Elements >> nth=0
await page.locator('text=Elements').first().click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
@ -77,7 +77,7 @@ test.describe('Grand Search', () => {
await expect(page.locator('.is-object-type-clock')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(createdObjects.displayLayout.name);
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText('Unnamed Display Layout');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C');
@ -185,7 +185,7 @@ async function createFolderObject(page, folderName) {
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folderName);
// Create folder object
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
}
async function waitForSearchCompletion(page) {
@ -197,56 +197,75 @@ async function waitForSearchCompletion(page) {
* Creates some domain objects for searching
* @param {import('@playwright/test').Page} page
*/
async function createObjectsForSearch(page) {
async function createObjectsForSearch(page, myItemsFolderName) {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const redFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Red Folder'
});
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Folder") >> nth=1').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Red Folder'),
await page.locator(`text=Save In Open MCT ${myItemsFolderName} >> span`).nth(3).click(),
page.locator('button:has-text("OK")').click()
]);
const blueFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Blue Folder',
parent: redFolder.uuid
});
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Folder") >> nth=2').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Blue Folder'),
await page.locator('form[name="mctForm"] >> text=Red Folder').click(),
page.locator('button:has-text("OK")').click()
]);
const clockA = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock A',
parent: blueFolder.uuid
});
const clockB = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock B',
parent: blueFolder.uuid
});
const clockC = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock C',
parent: blueFolder.uuid
});
const clockD = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock D',
parent: blueFolder.uuid
});
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock A'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock B'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
// Go back into edit mode for the display layout
await page.locator('button[title="Edit"]').click();
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock C'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
return {
redFolder,
blueFolder,
clockA,
clockB,
clockC,
clockD,
displayLayout
};
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock D'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
await Promise.all([
page.waitForNavigation(),
page.locator(`a:has-text("${myItemsFolderName}") >> nth=0`).click()
]);
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Display Layout")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
}

View File

@ -53,7 +53,7 @@ test.describe('Performance tests', () => {
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();

View File

@ -33,8 +33,7 @@ define([
dataRateInHz: 1,
randomness: 0,
phase: 0,
loadDelay: 0,
infinityValues: false
loadDelay: 0
};
function GeneratorProvider(openmct) {
@ -57,8 +56,7 @@ define([
'dataRateInHz',
'randomness',
'phase',
'loadDelay',
'infinityValues'
'loadDelay'
];
request = request || {};

View File

@ -76,10 +76,10 @@
name: data.name,
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues),
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues)
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
}
});
nextStep += step;
@ -117,7 +117,6 @@
var phase = request.phase;
var randomness = request.randomness;
var loadDelay = Math.max(request.loadDelay, 0);
var infinityValues = request.infinityValues;
var step = 1000 / dataRateInHz;
var nextStep = start - (start % step) + step;
@ -128,10 +127,10 @@
data.push({
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues),
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues)
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
});
}
@ -156,20 +155,12 @@
});
}
function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
if (infinityValues && Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
}
function cos(timestamp, period, amplitude, offset, phase, randomness) {
return amplitude
* Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}
function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
if (infinityValues && Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
}
function sin(timestamp, period, amplitude, offset, phase, randomness) {
return amplitude
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}

View File

@ -143,16 +143,6 @@ define([
"telemetry",
"loadDelay"
]
},
{
name: "Include Infinity Values",
control: "toggleSwitch",
cssClass: "l-input",
key: "infinityValues",
property: [
"telemetry",
"infinityValues"
]
}
],
initialize: function (object) {
@ -163,8 +153,7 @@ define([
dataRateInHz: 1,
phase: 0,
randomness: 0,
loadDelay: 0,
infinityValues: false
loadDelay: 0
};
}
});

View File

@ -1,16 +1,16 @@
{
"name": "openmct",
"version": "2.1.5-SNAPSHOT",
"version": "2.1.3-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.2",
"@percy/cli": "1.16.0",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.11.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.25.2",
"@types/jasmine": "4.3.1",
"@types/lodash": "4.14.191",
"babel-loader": "9.1.0",
"@types/jasmine": "4.3.0",
"@types/lodash": "4.14.186",
"babel-loader": "9.0.0",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
@ -19,17 +19,17 @@
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.30.0",
"eslint": "8.26.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.11.2",
"eslint-plugin-vue": "9.8.0",
"eslint-plugin-vue": "9.7.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
"jasmine-core": "4.5.0",
"jasmine-core": "4.4.0",
"karma": "6.3.20",
"karma-chrome-launcher": "3.1.1",
"karma-cli": "2.0.0",
@ -42,10 +42,10 @@
"karma-webpack": "5.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.7.2",
"mini-css-extract-plugin": "2.6.1",
"moment": "2.29.4",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.38",
"moment-timezone": "0.5.37",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.25.2",
@ -53,18 +53,19 @@
"plotly.js-gl2d-dist": "2.14.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sass": "1.56.1",
"sass": "1.55.0",
"sass-loader": "13.0.2",
"sinon": "15.0.1",
"sinon": "14.0.1",
"style-loader": "^3.3.1",
"typescript": "4.9.4",
"typescript": "4.8.4",
"uuid": "9.0.0",
"vue": "2.6.14",
"vue": "^3.1.0",
"@vue/compat": "^3.1.0",
"vue-eslint-parser": "9.1.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"vue-loader": "^16.0.0",
"@vue/compiler-sfc": "^3.1.0",
"webpack": "5.74.0",
"webpack-cli": "5.0.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.11.1",
"webpack-merge": "5.8.0"
},
@ -114,5 +115,6 @@
"ios_saf > 15"
],
"author": "",
"license": "Apache-2.0"
"license": "Apache-2.0",
"private": true
}

View File

@ -56,12 +56,17 @@ export default class Editor extends EventEmitter {
* Save any unsaved changes from this editing session. This will
* end the current transaction.
*/
async save() {
save() {
const transaction = this.openmct.objects.getActiveTransaction();
await transaction.commit();
this.editing = false;
this.emit('isEditing', false);
this.openmct.objects.endTransaction();
return transaction.commit()
.then(() => {
this.editing = false;
this.emit('isEditing', false);
this.openmct.objects.endTransaction();
}).catch(error => {
throw error;
});
}
/**

View File

@ -23,11 +23,13 @@
import FormController from './FormController';
import FormProperties from './components/FormProperties.vue';
import EventEmitter from 'EventEmitter';
import Vue from 'vue';
import _ from 'lodash';
export default class FormsAPI {
export default class FormsAPI extends EventEmitter {
constructor(openmct) {
super();
this.openmct = openmct;
this.formController = new FormController(openmct);
}
@ -90,75 +92,29 @@ export default class FormsAPI {
/**
* Show form inside an Overlay dialog with given form structure
* @public
* @param {Array<Section>} formStructure a form structure, array of section
* @param {Object} options
* @property {function} onChange a callback function when any changes detected
*/
showForm(formStructure, {
onChange
} = {}) {
let overlay;
const self = this;
const overlayEl = document.createElement('div');
overlayEl.classList.add('u-contents');
overlay = self.openmct.overlays.overlay({
element: overlayEl,
size: 'dialog'
});
let formSave;
let formCancel;
const promise = new Promise((resolve, reject) => {
formSave = resolve;
formCancel = reject;
});
this.showCustomForm(formStructure, {
element: overlayEl,
onChange
})
.then((response) => {
overlay.dismiss();
formSave(response);
})
.catch((response) => {
overlay.dismiss();
formCancel(response);
});
return promise;
}
/**
* Show form as a child of the element provided with given form structure
*
* @public
* @param {Array<Section>} formStructure a form structure, array of section
* @param {Object} options
* @property {HTMLElement} element Parent Element to render a Form
* @property {function} onChange a callback function when any changes detected
* @property {function} onSave a callback function when form is submitted
* @property {function} onDismiss a callback function when form is dismissed
*/
showCustomForm(formStructure, {
showForm(formStructure, {
element,
onChange
} = {}) {
if (element === undefined) {
throw Error('Required element parameter not provided');
}
const changes = {};
let overlay;
let onDismiss;
let onSave;
const self = this;
const changes = {};
let formSave;
let formCancel;
const promise = new Promise((resolve, reject) => {
formSave = onFormAction(resolve);
formCancel = onFormAction(reject);
onSave = onFormAction(resolve);
onDismiss = onFormAction(reject);
});
const vm = new Vue({
@ -170,17 +126,26 @@ export default class FormsAPI {
return {
formStructure,
onChange: onFormPropertyChange,
onCancel: formCancel,
onSave: formSave
onDismiss,
onSave
};
},
template: '<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
template: '<FormProperties :model="formStructure" @onChange="onChange" @onDismiss="onDismiss" @onSave="onSave"></FormProperties>'
}).$mount();
const formElement = vm.$el;
element.append(formElement);
if (element) {
element.append(formElement);
} else {
overlay = self.openmct.overlays.overlay({
element: vm.$el,
size: 'dialog',
onDestroy: () => vm.$destroy()
});
}
function onFormPropertyChange(data) {
self.emit('onFormPropertyChange', data);
if (onChange) {
onChange(data);
}
@ -193,14 +158,17 @@ export default class FormsAPI {
key = property.join('.');
}
_.set(changes, key, data.value);
changes[key] = data.value;
}
}
function onFormAction(callback) {
return () => {
formElement.remove();
vm.$destroy();
if (element) {
formElement.remove();
} else {
overlay.dismiss();
}
if (callback) {
callback(changes);

View File

@ -133,7 +133,7 @@ describe('The Forms API', () => {
});
it('when container element is provided', (done) => {
openmct.forms.showCustomForm(formStructure, { element }).catch(() => {
openmct.forms.showForm(formStructure, { element }).catch(() => {
done();
});
const titleElement = element.querySelector('.c-overlay__dialog-title');

View File

@ -73,7 +73,7 @@
tabindex="0"
class="c-button js-cancel-button"
aria-label="Cancel"
@click="onCancel"
@click="onDismiss"
>
{{ cancelLabel }}
</button>
@ -164,8 +164,8 @@ export default {
this.$emit('onChange', data);
},
onCancel() {
this.$emit('onCancel');
onDismiss() {
this.$emit('onDismiss');
},
onSave() {
this.$emit('onSave');

View File

@ -26,7 +26,6 @@
v-model="selected"
required="model.required"
name="mctControl"
:aria-label="model.ariaLabel || model.name"
@change="onChange($event)"
>
<option

View File

@ -27,7 +27,6 @@
:class="model.cssClass"
>
<textarea
:id="`${model.key}-textarea`"
v-model="field"
type="text"
:size="model.size"

View File

@ -29,7 +29,6 @@
<ToggleSwitch
id="switchId"
:checked="isChecked"
:name="model.name"
@change="toggleCheckBox"
/>
</span>

View File

@ -3,52 +3,39 @@
class="c-menu"
:class="options.menuClass"
>
<ul
v-if="options.actions.length && options.actions[0].length"
role="menu"
>
<ul v-if="options.actions.length && options.actions[0].length">
<template
v-for="(actionGroups, index) in options.actions"
>
<div
:key="index"
role="group"
<li
v-for="action in actionGroups"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
role="separator"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</div></template>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</template>
</ul>
<ul
v-else
role="menu"
>
<ul v-else>
<li
v-for="action in options.actions"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"

View File

@ -5,54 +5,45 @@
>
<ul
v-if="options.actions.length && options.actions[0].length"
role="menu"
class="c-super-menu__menu"
>
<template
v-for="(actionGroups, index) in options.actions"
>
<div
:key="index"
role="group"
<li
v-for="action in actionGroups"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
role="separator"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</div></template>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</template>
</ul>
<ul
v-else
class="c-super-menu__menu"
role="menu"
>
<li
v-for="action in options.actions"
:key="action.name"
role="menuitem"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"

View File

@ -19,7 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const DEFAULT_INTERCEPTOR_PRIORITY = 0;
export default class InterceptorRegistry {
/**
* A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.
@ -46,6 +45,7 @@ export default class InterceptorRegistry {
* @memberof module:openmct.InterceptorRegistry#
*/
addInterceptor(interceptorDef) {
//TODO: sort by priority
this.interceptors.push(interceptorDef);
}
@ -56,18 +56,10 @@ export default class InterceptorRegistry {
* @memberof module:openmct.InterceptorRegistry#
*/
getInterceptors(identifier, object) {
function byPriority(interceptorA, interceptorB) {
let priorityA = interceptorA.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
let priorityB = interceptorB.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
return priorityB - priorityA;
}
return this.interceptors.filter(interceptor => {
return typeof interceptor.appliesTo === 'function'
&& interceptor.appliesTo(identifier, object);
}).sort(byPriority);
});
}
}

View File

@ -75,7 +75,11 @@ class MutableDomainObject {
return eventOff;
}
$set(path, value) {
MutableDomainObject.mutateObject(this, path, value);
_.set(this, path, value);
if (path !== 'persisted' && path !== 'modified') {
_.set(this, 'modified', Date.now());
}
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
@ -132,11 +136,8 @@ class MutableDomainObject {
}
static mutateObject(object, path, value) {
if (path !== 'persisted') {
_.set(object, 'modified', Date.now());
}
_.set(object, path, value);
_.set(object, 'modified', Date.now());
}
}

View File

@ -357,13 +357,13 @@ export default class ObjectAPI {
async save(domainObject) {
const provider = this.getProvider(domainObject.identifier);
let result;
let lastPersistedTime;
if (!this.isPersistable(domainObject.identifier)) {
result = Promise.reject('Object provider does not support saving');
} else if (this.#hasAlreadyBeenPersisted(domainObject)) {
result = Promise.resolve(true);
} else {
const persistedTime = Date.now();
const username = await this.#getCurrentUsername();
const isNewObject = domainObject.persisted === undefined;
let savedResolve;
@ -375,23 +375,15 @@ export default class ObjectAPI {
savedReject = reject;
});
this.#mutate(domainObject, 'persisted', persistedTime);
this.#mutate(domainObject, 'modifiedBy', username);
if (isNewObject) {
this.#mutate(domainObject, 'created', persistedTime);
this.#mutate(domainObject, 'createdBy', username);
const createdTime = Date.now();
this.#mutate(domainObject, 'created', createdTime);
const persistedTime = Date.now();
this.#mutate(domainObject, 'persisted', persistedTime);
savedObjectPromise = provider.create(domainObject);
} else {
lastPersistedTime = domainObject.persisted;
const persistedTime = Date.now();
this.#mutate(domainObject, 'persisted', persistedTime);
savedObjectPromise = provider.update(domainObject);
}
@ -399,10 +391,6 @@ export default class ObjectAPI {
savedObjectPromise.then(response => {
savedResolve(response);
}).catch((error) => {
if (lastPersistedTime !== undefined) {
this.#mutate(domainObject, 'persisted', lastPersistedTime);
}
savedReject(error);
});
} else {

View File

@ -94,35 +94,6 @@ describe("The Object API", () => {
expect(mockProvider.create).not.toHaveBeenCalled();
expect(mockProvider.update).toHaveBeenCalled();
});
describe("the persisted timestamp for existing objects", () => {
let persistedTimestamp;
beforeEach(() => {
persistedTimestamp = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.persisted = persistedTimestamp;
mockDomainObject.modified = Date.now();
});
it("is updated", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted).toBeDefined();
expect(mockDomainObject.persisted > persistedTimestamp).toBe(true);
});
it("is >= modified timestamp", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true);
});
});
describe("the persisted timestamp for new objects", () => {
it("is updated", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted).toBeDefined();
});
it("is >= modified timestamp", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true);
});
});
it("Sets the current user for 'createdBy' on new objects", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.createdBy).toBe(USERNAME);

View File

@ -17,7 +17,6 @@ class Overlay extends EventEmitter {
dismissable = true,
element,
onDestroy,
onDismiss,
size
} = {}) {
super();
@ -33,7 +32,7 @@ class Overlay extends EventEmitter {
OverlayComponent: OverlayComponent
},
provide: {
dismiss: this.notifyAndDismiss.bind(this),
dismiss: this.dismiss.bind(this),
element,
buttons,
dismissable: this.dismissable
@ -44,10 +43,6 @@ class Overlay extends EventEmitter {
if (onDestroy) {
this.once('destroy', onDestroy);
}
if (onDismiss) {
this.once('dismiss', onDismiss);
}
}
dismiss() {
@ -56,12 +51,6 @@ class Overlay extends EventEmitter {
this.component.$destroy();
}
//Ensures that any callers are notified that the overlay is dismissed
notifyAndDismiss() {
this.emit('dismiss');
this.dismiss();
}
/**
* @private
**/

View File

@ -55,7 +55,7 @@ class OverlayAPI {
dismissLastOverlay() {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.notifyAndDismiss();
lastOverlay.dismiss();
}
}

View File

@ -202,13 +202,8 @@ class IndependentTimeContext extends TimeContext {
}
getUpstreamContext() {
const objectKey = this.openmct.objects.makeKeyString(this.objectPath[this.objectPath.length - 1].identifier);
const doesObjectHaveTimeContext = this.globalTimeContext.independentContexts.get(objectKey);
if (doesObjectHaveTimeContext) {
return undefined;
}
let timeContext = this.globalTimeContext;
this.objectPath.some((item, index) => {
const key = this.openmct.objects.makeKeyString(item.identifier);
//last index is the view object itself

View File

@ -33,9 +33,9 @@
<tbody>
<template
v-for="ladTable in ladTableObjects"
:key="ladTable.key"
>
<tr
:key="ladTable.key"
class="c-table__group-header js-lad-table-set__table-headers"
>
<td colspan="10">

View File

@ -112,7 +112,11 @@ export default {
}
},
removeFromComposition(telemetryObject) {
this.composition.remove(telemetryObject);
let composition = this.domainObject.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
);
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
},
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object

View File

@ -104,14 +104,10 @@ export default {
this.$set(this.plotSeries, this.plotSeries.length, series);
this.setAxesLabels();
},
removeSeries(seriesKey) {
const seriesIndex = this.plotSeries.findIndex(
plotSeries => this.openmct.objects.areIdsEqual(seriesKey, plotSeries.identifier)
);
const foundSeries = seriesIndex > -1;
if (foundSeries) {
this.$delete(this.plotSeries, seriesIndex);
removeSeries(series) {
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier));
if (index !== undefined) {
this.$delete(this.plotSeries, index);
this.setAxesLabels();
}
},

View File

@ -68,7 +68,6 @@ export default function ClockPlugin(options) {
]
},
{
ariaLabel: "12 or 24 hour clock",
control: 'select',
options: [
{

View File

@ -583,7 +583,6 @@ define(['lodash'], function (_) {
domainObject: selectedParent,
icon: "icon-object",
title: "Merge into a telemetry table or plot",
label: "View type",
options: APPLICABLE_VIEWS['telemetry-view-multi'],
method: function (option) {
displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value);

View File

@ -245,9 +245,6 @@ export default {
});
this.gridDimensions = [wMax * this.gridSize[0], hMax * this.gridSize[1]];
},
clearSelection() {
this.$el.click();
},
watchDisplayResize() {
const resizeObserver = new ResizeObserver(() => this.updateGrid());
@ -481,7 +478,7 @@ export default {
});
_.pullAt(this.layoutItems, indices);
this.mutate("configuration.items", this.layoutItems);
this.clearSelection();
this.$el.click();
},
untrackItem(item) {
if (!item.identifier) {
@ -507,11 +504,15 @@ export default {
}
if (!telemetryViewCount && !objectViewCount) {
this.removeFromComposition(item);
this.removeFromComposition(keyString);
}
},
removeFromComposition(item) {
this.composition.remove(item);
removeFromComposition(keyString) {
let composition = this.domainObject.composition ? this.domainObject.composition : [];
composition = composition.filter(identifier => {
return this.openmct.objects.makeKeyString(identifier) !== keyString;
});
this.mutate("composition", composition);
},
initializeItems() {
this.telemetryViewMap = {};
@ -528,10 +529,7 @@ export default {
}
});
this.startTransaction();
removedItems.forEach(this.removeFromConfiguration);
return this.endTransaction();
},
isItemAlreadyTracked(child) {
let found = false;
@ -592,7 +590,7 @@ export default {
}
});
this.mutate("configuration.items", layoutItems);
this.clearSelection();
this.$el.click();
},
orderItem(position, selectedItems) {
let delta = ORDERS[position];
@ -775,7 +773,7 @@ export default {
this.$nextTick(() => {
this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
this.openmct.objects.mutate(this.domainObject, "configuration.objectStyles", objectStyles);
this.clearSelection();
this.$el.click(); //clear selection;
newDomainObjectsArray.forEach(domainObject => {
this.composition.add(domainObject);
@ -869,20 +867,6 @@ export default {
this.removeItem(selection);
this.initSelectIndex = this.layoutItems.length - 1; //restore selection
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
}
},
async endTransaction() {
if (!this.transaction) {
return;
}
await this.transaction.commit();
this.openmct.objects.endTransaction();
this.transaction = null;
},
toggleGrid() {
this.showGrid = !this.showGrid;
},

View File

@ -45,9 +45,9 @@
<div class="c-fl-container__frames-holder">
<template
v-for="(frame, i) in frames"
:key="frame.id"
>
<frame-component
:key="frame.id"
class="c-fl-container__frame"
:frame="frame"
:index="i"
@ -57,7 +57,6 @@
/>
<drop-hint
:key="'hint-' + i"
class="c-fl-frame__drop-hint"
:index="i"
:allow-drop="allowDrop"
@ -66,7 +65,6 @@
<resize-handle
v-if="(i !== frames.length - 1)"
:key="'handle-' + i"
:index="i"
:orientation="rowsLayout ? 'horizontal' : 'vertical'"
:is-editing="isEditing"

View File

@ -40,10 +40,12 @@
'c-fl--rows': rowsLayout === true
}"
>
<template v-for="(container, index) in containers">
<template
v-for="(container, index) in containers"
:key="`component-${container.id}`"
>
<drop-hint
v-if="index === 0 && containers.length > 1"
:key="`hint-top-${container.id}`"
class="c-fl-frame__drop-hint"
:index="-1"
:allow-drop="allowContainerDrop"
@ -51,7 +53,6 @@
/>
<container-component
:key="`component-${container.id}`"
class="c-fl__container"
:index="index"
:container="container"
@ -77,7 +78,6 @@
<drop-hint
v-if="containers.length > 1"
:key="`hint-bottom-${container.id}`"
class="c-fl-frame__drop-hint"
:index="index"
:allow-drop="allowContainerDrop"
@ -185,24 +185,10 @@ export default {
this.composition.off('add', this.addFrame);
},
methods: {
containsObject(identifier) {
if ('composition' in this.domainObject) {
return this.domainObject.composition
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
}
return false;
},
buildIdentifierMap() {
this.containers.forEach(container => {
container.frames.forEach(frame => {
if (!this.containsObject(frame.domainObjectIdentifier)) {
this.removeChildObject(frame.domainObjectIdentifier);
return;
}
const keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
let keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
this.identifierMap[keystring] = true;
});
});
@ -310,14 +296,11 @@ export default {
}
},
persist(index) {
this.startTransaction();
if (index) {
this.openmct.objects.mutate(this.domainObject, `configuration.containers[${index}]`, this.containers[index]);
} else {
this.openmct.objects.mutate(this.domainObject, 'configuration.containers', this.containers);
}
return this.endTransaction();
},
startContainerResizing(index) {
let beforeContainer = this.containers[index];
@ -383,20 +366,6 @@ export default {
});
this.persist();
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
}
},
async endTransaction() {
if (!this.transaction) {
return;
}
await this.transaction.commit();
this.openmct.objects.endTransaction();
this.transaction = null;
}
}
};

View File

@ -24,7 +24,6 @@ import PropertiesAction from './PropertiesAction';
import CreateWizard from './CreateWizard';
import { v4 as uuid } from 'uuid';
import _ from 'lodash';
export default class CreateAction extends PropertiesAction {
constructor(openmct, type, parentDomainObject) {
@ -51,12 +50,19 @@ export default class CreateAction extends PropertiesAction {
return;
}
const existingValue = this.domainObject[key];
if (!(existingValue instanceof Array) && (typeof existingValue === 'object')) {
value = _.merge(existingValue, value);
}
const properties = key.split('.');
let object = this.domainObject;
const propertiesLength = properties.length;
properties.forEach((property, index) => {
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
if (isComplexProperty && object[property] !== null) {
object = object[property];
} else {
object[property] = value;
}
});
_.set(this.domainObject, key, value);
object = value;
});
const parentDomainObject = parentDomainObjectPath[0];
@ -88,12 +94,6 @@ export default class CreateAction extends PropertiesAction {
dialog.dismiss();
}
/**
* @private
*/
_onCancel() {
//do Nothing
}
/**
* @private
*/
@ -151,7 +151,6 @@ export default class CreateAction extends PropertiesAction {
formStructure.title = 'Create a New ' + definition.name;
this.openmct.forms.showForm(formStructure)
.then(this._onSave.bind(this))
.catch(this._onCancel.bind(this));
.then(this._onSave.bind(this));
}
}

View File

@ -22,8 +22,6 @@
import PropertiesAction from './PropertiesAction';
import CreateWizard from './CreateWizard';
import _ from 'lodash';
export default class EditPropertiesAction extends PropertiesAction {
constructor(openmct) {
super(openmct);
@ -53,23 +51,25 @@ export default class EditPropertiesAction extends PropertiesAction {
/**
* @private
*/
async _onSave(changes) {
if (!this.openmct.objects.isTransactionActive()) {
this.openmct.objects.startTransaction();
}
_onSave(changes) {
try {
Object.entries(changes).forEach(([key, value]) => {
const existingValue = this.domainObject[key];
if (!(Array.isArray(existingValue)) && (typeof existingValue === 'object')) {
value = _.merge(existingValue, value);
}
const properties = key.split('.');
let object = this.domainObject;
const propertiesLength = properties.length;
properties.forEach((property, index) => {
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
if (isComplexProperty && object[property] !== null) {
object = object[property];
} else {
object[property] = value;
}
});
object = value;
this.openmct.objects.mutate(this.domainObject, key, value);
this.openmct.notifications.info('Save successful');
});
const transaction = this.openmct.objects.getActiveTransaction();
await transaction.commit();
this.openmct.objects.endTransaction();
} catch (error) {
this.openmct.notifications.error('Error saving objects');
console.error(error);

View File

@ -24,7 +24,6 @@ import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import Vue from 'vue';
import { debounce } from 'lodash';
@ -102,15 +101,10 @@ describe('EditPropertiesAction plugin', () => {
composition: []
};
editPropertiesAction.invoke([domainObject])
.then(() => {
done();
})
.catch(() => {
done();
});
const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
Vue.nextTick(() => {
function handleFormPropertyChange(data) {
const form = document.querySelector('.js-form');
const title = form.querySelector('input');
expect(title.value).toEqual(domainObject.name);
@ -124,7 +118,17 @@ describe('EditPropertiesAction plugin', () => {
const clickEvent = createMouseEvent('click');
buttons[1].dispatchEvent(clickEvent);
});
openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
}
editPropertiesAction.invoke([domainObject])
.then(() => {
done();
})
.catch(() => {
done();
});
});
it('edit properties action saves changes', (done) => {
@ -155,9 +159,11 @@ describe('EditPropertiesAction plugin', () => {
const deBouncedCallback = debounce(callback, 300);
unObserve = openmct.objects.observe(domainObject, '*', deBouncedCallback);
editPropertiesAction.invoke([domainObject]);
let changed = false;
const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
Vue.nextTick(() => {
function handleFormPropertyChange(data) {
const form = document.querySelector('.js-form');
const title = form.querySelector('input');
const notes = form.querySelector('textArea');
@ -166,18 +172,27 @@ describe('EditPropertiesAction plugin', () => {
expect(buttons[0].textContent.trim()).toEqual('OK');
expect(buttons[1].textContent.trim()).toEqual('Cancel');
expect(title.value).toEqual(domainObject.name);
expect(notes.value).toEqual(domainObject.notes);
if (!changed) {
expect(title.value).toEqual(domainObject.name);
expect(notes.value).toEqual(domainObject.notes);
// change input field value and dispatch event for it
title.focus();
title.value = newName;
title.dispatchEvent(new Event('input'));
title.blur();
// change input field value and dispatch event for it
title.focus();
title.value = newName;
title.dispatchEvent(new Event('input'));
title.blur();
const clickEvent = createMouseEvent('click');
buttons[0].dispatchEvent(clickEvent);
});
changed = true;
} else {
// click ok to save form changes
const clickEvent = createMouseEvent('click');
buttons[0].dispatchEvent(clickEvent);
openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
}
}
editPropertiesAction.invoke([domainObject]);
});
it('edit properties action discards changes', (done) => {
@ -202,6 +217,7 @@ describe('EditPropertiesAction plugin', () => {
})
.catch(() => {
expect(domainObject.name).toEqual(name);
done();
});

View File

@ -598,7 +598,11 @@ export default {
return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2);
},
removeFromComposition(telemetryObject = this.telemetryObject) {
this.composition.remove(telemetryObject);
let composition = this.domainObject.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
);
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
},
refreshData(bounds, isTick) {
if (!isTick) {

View File

@ -100,7 +100,6 @@ export default {
components: {
ToggleSwitch
},
inject: ["openmct"],
props: {
model: {
type: Object,
@ -108,10 +107,11 @@ export default {
}
},
data() {
this.changes = {};
return {
isUseTelemetryLimits: this.model.value.isUseTelemetryLimits,
isDisplayMinMax: this.model.value.isDisplayMinMax,
isDisplayCurVal: this.model.value.isDisplayCurVal,
isDisplayUnits: this.model.value.isDisplayUnits,
limitHigh: this.model.value.limitHigh,
limitLow: this.model.value.limitLow,
max: this.model.value.max,
@ -120,15 +120,24 @@ export default {
},
methods: {
onChange(event) {
let data = {
model: {}
const data = {
model: this.model,
value: {
gaugeType: this.model.value.gaugeType,
isDisplayMinMax: this.isDisplayMinMax,
isDisplayCurVal: this.isDisplayCurVal,
isDisplayUnits: this.isDisplayUnits,
isUseTelemetryLimits: this.isUseTelemetryLimits,
limitLow: this.limitLow,
limitHigh: this.limitHigh,
max: this.max,
min: this.min,
precision: this.model.value.precision
}
};
if (event) {
const target = event.target;
const property = target.dataset.fieldName;
data.model.property = Array.from(this.model.property).concat([property]);
data.value = this[property];
const targetIndicator = target.parentElement.querySelector('.req-indicator');
if (targetIndicator.classList.contains('req')) {
targetIndicator.classList.add('visited');
@ -151,13 +160,13 @@ export default {
},
toggleUseTelemetryLimits() {
this.isUseTelemetryLimits = !this.isUseTelemetryLimits;
const data = {
model: {
property: Array.from(this.model.property).concat(['isUseTelemetryLimits'])
},
value: this.isUseTelemetryLimits
};
this.$emit('onChange', data);
this.onChange();
},
toggleMinMax() {
this.isDisplayMinMax = !this.isDisplayMinMax;
this.onChange();
}
}
};

View File

@ -45,10 +45,6 @@ export default class GoToOriginalAction {
});
}
appliesTo(objectPath) {
if (this._openmct.editor.isEditing()) {
return false;
}
let parentKeystring = objectPath[1] && this._openmct.objects.makeKeyString(objectPath[1].identifier);
if (!parentKeystring) {

View File

@ -31,32 +31,21 @@
:title="image.formattedTime"
>
<a
class="c-thumb__image-wrapper"
href=""
:download="image.imageDownloadName"
@click.prevent
>
<img
ref="img"
class="c-thumb__image"
:src="image.url"
fetchpriority="low"
@load="imageLoadCompleted"
>
</a>
<div
v-if="viewableArea"
class="c-thumb__viewable-area"
:style="viewableAreaStyle"
></div>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</template>
<script>
const THUMB_PADDING = 4;
const BORDER_WIDTH = 2;
export default {
props: {
image: {
@ -74,77 +63,6 @@ export default {
realTime: {
type: Boolean,
required: true
},
viewableArea: {
type: Object,
default: function () {
return null;
}
}
},
data() {
return {
imgWidth: 0,
imgHeight: 0
};
},
computed: {
viewableAreaStyle() {
if (!this.viewableArea || !this.imgWidth || !this.imgHeight) {
return null;
}
const { widthRatio, heightRatio, xOffsetRatio, yOffsetRatio } = this.viewableArea;
const imgWidth = this.imgWidth;
const imgHeight = this.imgHeight;
let translateX = imgWidth * xOffsetRatio;
let translateY = imgHeight * yOffsetRatio;
let width = imgWidth * widthRatio;
let height = imgHeight * heightRatio;
if (translateX < 0) {
width += translateX;
translateX = 0;
}
if (translateX + width > imgWidth) {
width = imgWidth - translateX;
}
if (translateX + 2 * BORDER_WIDTH > imgWidth) {
translateX = imgWidth - 2 * BORDER_WIDTH;
}
if (translateY < 0) {
height += translateY;
translateY = 0;
}
if (translateY + height > imgHeight) {
height = imgHeight - translateY;
}
if (translateY + 2 * BORDER_WIDTH > imgHeight) {
translateY = imgHeight - 2 * BORDER_WIDTH;
}
return {
'transform': `translate(${translateX + THUMB_PADDING}px, ${translateY + THUMB_PADDING}px)`,
'width': `${width}px`,
'height': `${height}px`
};
}
},
methods: {
imageLoadCompleted() {
if (!this.$refs.img) {
return;
}
const { width: imgWidth, height: imgHeight } = this.$refs.img;
this.imgWidth = imgWidth;
this.imgHeight = imgHeight;
}
}
};

View File

@ -256,7 +256,7 @@ export default {
isNested: true
};
},
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><div class="c-imagery-tsv-container"></div></template></swim-lane>`
template: `<swim-lane :is-nested="isNested" :hide-label="true"><slot name="object"><div class="c-imagery-tsv-container"></div></slot></swim-lane>`
});
this.$refs.imageryHolder.appendChild(component.$mount().$el);

View File

@ -25,7 +25,7 @@
tabindex="0"
class="c-imagery"
@keyup="arrowUpHandler"
@keydown.prevent="arrowDownHandler"
@keydown="arrowDownHandler"
@mouseover="focusElement"
>
<div
@ -147,7 +147,7 @@
v-if="!isFixed"
class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}"
@click="handlePauseButton(!isPaused)"
@click="paused(!isPaused)"
></button>
</div>
</div>
@ -165,9 +165,6 @@
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-scroll-area"
:class="[{
'animate-scroll': animateThumbScroll
}]"
@scroll="handleScroll"
>
<ImageThumbnail
@ -177,15 +174,14 @@
:active="focusedImageIndex === index"
:selected="focusedImageIndex === index && isPaused"
:real-time="!isFixed"
:viewable-area="focusedImageIndex === index ? viewableArea : null"
@click.native="thumbnailClicked(index)"
@click="thumbnailClicked(index)"
/>
</div>
<button
class="c-imagery__auto-scroll-resume-button c-icon-button icon-play"
title="Resume automatic scrolling of image thumbnails"
@click="scrollToRight"
@click="scrollToRight('reset')"
></button>
</div>
</div>
@ -195,7 +191,6 @@
import eventHelpers from '../lib/eventHelpers';
import _ from 'lodash';
import moment from 'moment';
import Vue from 'vue';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
@ -224,8 +219,6 @@ const ZOOM_SCALE_DEFAULT = 1;
const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
const IMAGE_CONTAINER_BORDER_WIDTH = 1;
export default {
name: 'ImageryView',
components: {
@ -288,13 +281,10 @@ export default {
},
imageTranslateX: 0,
imageTranslateY: 0,
imageViewportWidth: 0,
imageViewportHeight: 0,
pan: undefined,
animateZoom: true,
imagePanned: false,
forceShowThumbnails: false,
animateThumbScroll: false
forceShowThumbnails: false
};
},
computed: {
@ -398,12 +388,6 @@ export default {
return disabled;
},
isComposedInLayout() {
return (
this.currentView?.objectPath
&& !this.openmct.router.isNavigatedObject(this.currentView.objectPath)
);
},
focusedImage() {
return this.imageHistory[this.focusedImageIndex];
},
@ -532,28 +516,11 @@ export default {
}
return 'Alt drag to pan';
},
viewableArea() {
if (this.zoomFactor === 1) {
return null;
}
const imageWidth = this.sizedImageWidth * this.zoomFactor;
const imageHeight = this.sizedImageHeight * this.zoomFactor;
const xOffset = (imageWidth - this.imageViewportWidth) / 2;
const yOffset = (imageHeight - this.imageViewportHeight) / 2;
return {
widthRatio: this.imageViewportWidth / imageWidth,
heightRatio: this.imageViewportHeight / imageHeight,
xOffsetRatio: (xOffset - this.imageTranslateX * this.zoomFactor) / imageWidth,
yOffsetRatio: (yOffset - this.imageTranslateY * this.zoomFactor) / imageHeight
};
}
},
watch: {
imageHistory: {
async handler(newHistory, oldHistory) {
handler(newHistory, _oldHistory) {
const newSize = newHistory.length;
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
if (this.focusedImageTimestamp !== undefined) {
@ -581,13 +548,10 @@ export default {
if (!this.isPaused) {
this.setFocusedImage(imageIndex);
this.scrollToRight();
} else {
this.scrollToFocused();
}
await this.scrollHandler();
if (oldHistory?.length > 0) {
this.animateThumbScroll = true;
}
},
deep: true
},
@ -598,7 +562,7 @@ export default {
this.getImageNaturalDimensions();
},
bounds() {
this.scrollHandler();
this.scrollToFocused();
},
isFixed(newValue) {
const isRealTime = !newValue;
@ -862,13 +826,6 @@ export default {
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
this.autoScroll = !disableScroll;
},
handlePauseButton(newState) {
this.paused(newState);
if (newState) {
// need to set the focused index or the paused focus will drift
this.thumbnailClicked(this.focusedImageIndex);
}
},
paused(state) {
this.isPaused = Boolean(state);
@ -876,7 +833,7 @@ export default {
this.previousFocusedImage = null;
this.setFocusedImage(this.nextImageIndex);
this.autoScroll = true;
this.scrollHandler();
this.scrollToRight();
}
},
scrollToFocused() {
@ -886,43 +843,28 @@ export default {
}
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
if (!domThumb) {
return;
if (domThumb) {
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
// separate scrollTo function had to be implemented since scrollIntoView
// caused undesirable behavior in layouts
// and could not simply be scoped to the parent element
if (this.isComposedInLayout) {
const wrapperWidth = this.$refs.thumbsWrapper.clientWidth ?? 0;
this.$refs.thumbsWrapper.scrollLeft = (
domThumb.offsetLeft - (wrapperWidth - domThumb.clientWidth) / 2);
return;
}
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
},
async scrollToRight() {
scrollToRight(type) {
if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
return;
}
const scrollWidth = this.$refs?.thumbsWrapper?.scrollWidth ?? 0;
const scrollWidth = this.$refs.thumbsWrapper.scrollWidth || 0;
if (!scrollWidth) {
return;
}
await Vue.nextTick();
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
},
scrollHandler() {
if (this.isPaused) {
return this.scrollToFocused();
} else if (this.autoScroll) {
return this.scrollToRight();
}
this.$nextTick(() => {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
},
matchIndexOfPreviousImage(previous, imageHistory) {
// match logic uses a composite of url and time to account
@ -1121,12 +1063,12 @@ export default {
}
this.setSizedImageDimensions();
this.setImageViewport();
this.calculateViewHeight();
this.scrollHandler();
this.scrollToFocused();
},
setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
// container is wider than image
this.sizedImageWidth = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
@ -1137,17 +1079,6 @@ export default {
this.sizedImageHeight = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;
}
},
setImageViewport() {
if (this.imageContainerHeight > this.sizedImageHeight + IMAGE_CONTAINER_BORDER_WIDTH) {
// container is taller than wrapper
this.imageViewportWidth = this.sizedImageWidth;
this.imageViewportHeight = this.sizedImageHeight;
} else {
// container is wider than wrapper
this.imageViewportWidth = this.imageContainerWidth;
this.imageViewportHeight = this.imageContainerHeight;
}
},
handleThumbWindowResizeStart() {
if (!this.autoScroll) {
return;
@ -1158,7 +1089,9 @@ export default {
this.handleThumbWindowResizeEnded();
},
handleThumbWindowResizeEnded() {
this.scrollHandler();
if (!this.isPaused) {
this.scrollToRight('reset');
}
this.calculateViewHeight();
@ -1171,6 +1104,7 @@ export default {
},
wheelZoom(e) {
e.preventDefault();
this.$refs.imageControls.wheelZoom(e);
},
startPan(e) {

View File

@ -194,9 +194,6 @@
overflow-y: hidden;
margin-bottom: 1px;
padding-bottom: $interiorMarginSm;
&.animate-scroll {
scroll-behavior: smooth;
}
}
&__auto-scroll-resume-button {
@ -288,13 +285,6 @@
flex: 0 0 auto;
padding: 2px 3px;
}
&__viewable-area {
position: absolute;
border: 2px yellow solid;
left: 0;
top: 0;
}
}
.is-small-thumbs {

View File

@ -481,16 +481,19 @@ describe("The Imagery View Layouts", () => {
});
});
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
await Vue.nextTick();
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollHandler');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollHandler);
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
Vue.nextTick(() => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
Vue.nextTick(() => {
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
done();
});
});
});
xit('should change the image zoom factor when using the zoom buttons', async () => {
xit('should change the image zoom factor when using the zoom buttons', async (done) => {
await Vue.nextTick();
let imageSizeBefore;
let imageSizeAfter;
@ -509,6 +512,7 @@ describe("The Imagery View Layouts", () => {
imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
done();
});
xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {
await Vue.nextTick();
@ -525,19 +529,6 @@ describe("The Imagery View Layouts", () => {
done();
});
it('should display the viewable area when zoom factor is greater than 1', async () => {
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);
parent.querySelector('.t-btn-zoom-in').click();
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(1);
parent.querySelector('.t-btn-zoom-reset').click();
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);
});
it('should reset the brightness and contrast when clicking the reset button', async () => {
const viewInstance = imageryView._getInstance();
await Vue.nextTick();

View File

@ -37,15 +37,14 @@ function myItemsInterceptor(openmct, identifierObject, name) {
return identifier.key === MY_ITEMS_KEY;
},
invoke: (identifier, object) => {
if (!object || openmct.objects.isMissing(object)) {
if (openmct.objects.isMissing(object)) {
openmct.objects.save(myItemsModel);
return myItemsModel;
}
return object;
},
priority: openmct.priority.HIGH
}
};
}

View File

@ -889,21 +889,37 @@ export default {
this.syncUrlWithPageAndSection();
this.filterAndSortEntries();
},
activeTransaction() {
return this.openmct.objects.getActiveTransaction();
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
if (!this.openmct.editor.isEditing()) {
this.openmct.objects.startTransaction();
}
},
async saveTransaction() {
if (this.transaction !== undefined) {
await this.transaction.commit();
this.openmct.objects.endTransaction();
saveTransaction() {
const transaction = this.activeTransaction();
if (!transaction || this.openmct.editor.isEditing()) {
return;
}
return transaction.commit()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
},
async cancelTransaction() {
if (this.transaction !== undefined) {
await this.transaction.cancel();
this.openmct.objects.endTransaction();
cancelTransaction() {
if (!this.openmct.editor.isEditing()) {
const transaction = this.activeTransaction();
transaction.cancel()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
}
}
}

View File

@ -31,8 +31,8 @@ export default class OpenInNewTab {
this._openmct = openmct;
}
invoke(objectPath, urlParams = undefined) {
let url = objectPathToUrl(this._openmct, objectPath, urlParams);
invoke(objectPath) {
let url = objectPathToUrl(this._openmct, objectPath);
window.open(url);
}
}

View File

@ -27,7 +27,7 @@
>
<template v-if="viewBounds && !options.compact">
<swim-lane>
<template slot="label">{{ timeSystem.name }}</template>
<slot name="label">{{ timeSystem.name }}</slot>
<timeline-axis
slot="object"
:bounds="viewBounds"
@ -418,7 +418,7 @@ export default {
width: svgWidth
};
},
template: `<swim-lane :is-nested="isNested" :status="status"><template slot="label">{{heading}}</template><template slot="object"><svg :height="height" :width="width"></svg></template></swim-lane>`
template: `<swim-lane :is-nested="isNested" :status="status"><slot name="label">{{heading}}</template><template slot="object"><svg :height="height" :width="width"></svg></slot></swim-lane>`
});
this.$refs.planHolder.appendChild(component.$mount().$el);

View File

@ -383,8 +383,10 @@ export default {
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.followTimeContext();
},
followTimeContext() {
this.updateDisplayBounds(this.timeContext.bounds());
@ -1190,15 +1192,11 @@ export default {
this.$emit('statusUpdated', status);
},
handleWindowResize() {
const { plotWrapper } = this.$parent.$refs;
if (!plotWrapper) {
return;
}
const newOffsetWidth = plotWrapper.offsetWidth;
const newOffsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
//we ignore when width gets smaller
const offsetChange = newOffsetWidth - this.offsetWidth;
if (offsetChange > OFFSET_THRESHOLD) {
if (this.$parent.$refs.plotWrapper
&& offsetChange > OFFSET_THRESHOLD) {
this.offsetWidth = newOffsetWidth;
this.config.series.models.forEach(this.loadSeriesData, this);
}

View File

@ -83,8 +83,6 @@ export default class PlotSeries extends Model {
// Model.apply(this, arguments);
this.onXKeyChange(this.get('xKey'));
this.onYKeyChange(this.get('yKey'));
this.unPlottableValues = [undefined, Infinity, -Infinity];
}
/**
@ -344,10 +342,6 @@ export default class PlotSeries extends Model {
let stats = this.get('stats');
let changed = false;
if (!stats) {
if ([Infinity, -Infinity].includes(value)) {
return;
}
stats = {
minValue: value,
minPoint: point,
@ -356,13 +350,13 @@ export default class PlotSeries extends Model {
};
changed = true;
} else {
if (stats.maxValue < value && value !== Infinity) {
if (stats.maxValue < value) {
stats.maxValue = value;
stats.maxPoint = point;
changed = true;
}
if (stats.minValue > value && value !== -Infinity) {
if (stats.minValue > value) {
stats.minValue = value;
stats.minPoint = point;
changed = true;
@ -425,7 +419,7 @@ export default class PlotSeries extends Model {
* @private
*/
isValueInvalid(val) {
return Number.isNaN(val) || this.unPlottableValues.includes(val);
return Number.isNaN(val) || val === undefined;
}
/**

View File

@ -35,7 +35,6 @@ export default class RemoveAction {
this.openmct = openmct;
this.removeFromComposition = this.removeFromComposition.bind(this); // for access to private transaction variable
this.#transaction = null;
}
async invoke(objectPath) {
@ -153,13 +152,16 @@ export default class RemoveAction {
}
}
async saveTransaction() {
saveTransaction() {
if (!this.#transaction) {
return;
}
await this.#transaction.commit();
this.openmct.objects.endTransaction();
this.#transaction = null;
return this.#transaction.commit()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
}
}

View File

@ -78,8 +78,6 @@ describe("The Remove Action plugin", () => {
spyOn(removeAction, 'removeFromComposition').and.callThrough();
spyOn(removeAction, 'inNavigationPath').and.returnValue(false);
spyOn(openmct.objects, 'mutate').and.callThrough();
spyOn(openmct.objects, 'startTransaction').and.callThrough();
spyOn(openmct.objects, 'endTransaction').and.callThrough();
removeAction.removeFromComposition(parentObject, childObject);
});
@ -92,17 +90,6 @@ describe("The Remove Action plugin", () => {
expect(openmct.objects.mutate).toHaveBeenCalled();
expect(openmct.objects.mutate.calls.argsFor(0)[0]).toEqual(parentObject);
});
it("it should start a transaction", () => {
expect(openmct.objects.startTransaction).toHaveBeenCalled();
});
it("it should end the transaction", (done) => {
setTimeout(() => {
expect(openmct.objects.endTransaction).toHaveBeenCalled();
done();
}, 100);
});
});
describe("when determining the object is applicable", () => {

View File

@ -178,7 +178,7 @@ define([
if (this.paused) {
this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));
} else {
this.tableRows.addRows(telemetryRows);
this.tableRows.addRows(telemetryRows, 'add');
}
};
}
@ -229,7 +229,7 @@ define([
});
});
this.tableRows.clearRowsFromTableAndFilter(allRows);
this.tableRows.addRows(allRows, 'filter');
}
updateFilters(updatedFilters) {

View File

@ -61,39 +61,30 @@ define(
this.emit('remove', removed);
}
addRows(rows) {
let rowsToAdd = this.filterRows(rows);
addRows(rows, type = 'add') {
if (this.sortOptions === undefined) {
throw 'Please specify sort options';
}
let isFilterTriggeredReset = type === 'filter';
let anyActiveFilters = Object.keys(this.columnFilters).length > 0;
let rowsToAdd = !anyActiveFilters ? rows : rows.filter(this.matchesFilters, this);
// if type is filter, then it's a reset of all rows,
// need to wipe current rows
if (isFilterTriggeredReset) {
this.rows = [];
}
this.sortAndMergeRows(rowsToAdd);
// we emit filter no matter what to trigger
// an update of visible rows
if (rowsToAdd.length > 0) {
this.emit('add', rowsToAdd);
if (rowsToAdd.length > 0 || isFilterTriggeredReset) {
this.emit(type, rowsToAdd);
}
}
clearRowsFromTableAndFilter(rows) {
let rowsToAdd = this.filterRows(rows);
// Reset of all rows, need to wipe current rows
this.rows = [];
this.sortAndMergeRows(rowsToAdd);
// We emit filter and update of visible rows
this.emit('filter', rowsToAdd);
}
filterRows(rows) {
if (Object.keys(this.columnFilters).length > 0) {
return rows.filter(this.matchesFilters, this);
}
return rows;
}
sortAndMergeRows(rows) {
const sortedRowsToAdd = this.sortCollection(rows);

View File

@ -257,11 +257,11 @@
@change-height="setRowHeight"
/>
<tr>
<template v-for="(title, key) in headers">
<th
:key="key"
:style="{ width: configuredColumnWidths[key] + 'px', 'max-width': configuredColumnWidths[key] + 'px'}"
>
<template
v-for="(title, key) in headers"
:key="key"
>
<th :style="{ width: configuredColumnWidths[key] + 'px', 'max-width': configuredColumnWidths[key] + 'px'}">
{{ title }}
</th>
</template>

View File

@ -14,7 +14,7 @@
:bottom="keyString !== undefined"
:type="'start'"
:offset="offsets.start"
@focus.native="$event.target.select()"
@focus="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
@ -54,7 +54,7 @@
:bottom="keyString !== undefined"
:type="'end'"
:offset="offsets.end"
@focus.native="$event.target.select()"
@focus="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>

View File

@ -24,27 +24,9 @@
* Module defining url handling.
*/
function getUrlParams(openmct, customUrlParams = {}) {
let urlParams = openmct.router.getParams();
Object.entries(customUrlParams).forEach((urlParam) => {
const [key, value] = urlParam;
urlParams[key] = value;
});
if (urlParams['tc.mode'] === 'fixed') {
delete urlParams['tc.startDelta'];
delete urlParams['tc.endDelta'];
} else if (urlParams['tc.mode'] === 'local') {
delete urlParams['tc.startBound'];
delete urlParams['tc.endBound'];
}
return urlParams;
}
export function paramsToArray(openmct, customUrlParams = {}) {
export function paramsToArray(openmct) {
// parse urlParams from an object to an array.
let urlParams = getUrlParams(openmct, customUrlParams);
let urlParams = openmct.router.getParams();
let newTabParams = [];
for (let key in urlParams) {
if ({}.hasOwnProperty.call(urlParams, key)) {
@ -60,9 +42,9 @@ export function identifierToString(openmct, objectPath) {
return '#/browse/' + openmct.objects.getRelativePath(objectPath);
}
export default function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
export default function objectPathToUrl(openmct, objectPath) {
let url = identifierToString(openmct, objectPath);
let urlParams = paramsToArray(openmct, customUrlParams);
let urlParams = paramsToArray(openmct);
if (urlParams.length) {
url += '?' + urlParams.join('&');
}

View File

@ -66,14 +66,5 @@ describe('the url tool', function () {
const constructedURL = objectPathToUrl(openmct, mockObjectPath);
expect(constructedURL).toContain('#/browse/mock-parent-folder/mock-folder');
});
it('can take params to set a custom url', () => {
const customParams = {
'tc.startBound': 1669911059,
'tc.endBound': 1669911082,
'tc.mode': 'fixed'
};
const constructedURL = objectPathToUrl(openmct, mockObjectPath, customParams);
expect(constructedURL).toContain('tc.startBound=1669911059&tc.endBound=1669911082&tc.mode=fixed');
});
});
});

View File

@ -7,11 +7,7 @@
:checked="checked"
@change="onUserSelect($event)"
>
<span
class="c-toggle-switch__slider"
role="switch"
:aria-label="name"
></span>
<span class="c-toggle-switch__slider"></span>
</label>
<div
v-if="label && label.length"
@ -36,11 +32,6 @@ export default {
required: false,
default: ''
},
name: {
type: String,
required: false,
default: ''
},
checked: Boolean
},
methods: {

View File

@ -22,10 +22,8 @@
import ObjectView from './ObjectView.vue';
import StackedPlot from '../../plugins/plot/stackedPlot/StackedPlot.vue';
import Plot from '../../plugins/plot/Plot.vue';
export default {
ObjectView,
StackedPlot,
Plot
StackedPlot
};

View File

@ -5,10 +5,10 @@
>
<input
class="c-search__input"
v-bind="$attrs"
aria-label="Search Input"
tabindex="10000"
type="search"
v-bind="$attrs"
:value="value"
v-on="inputListeners"
>

View File

@ -76,7 +76,7 @@
<div :style="childrenHeightStyles">
<tree-item
v-for="(treeItem, index) in visibleItems"
:key="`${treeItem.navigationPath}-${index}`"
:key="treeItem.navigationPath"
:node="treeItem"
:is-selector-tree="isSelectorTree"
:selected-item="selectedItem"
@ -792,17 +792,12 @@ export default {
for (const result of results) {
if (!abortSignal.aborted) {
// Don't show deleted objects in search results
if (result.location === null) {
continue;
}
resultPromises.push(this.openmct.objects.getOriginalPath(result.identifier).then((objectPath) => {
// removing the item itself, as the path we pass to buildTreeItem is a parent path
objectPath.shift();
// if root, remove, we're not using in object path for tree
const lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
if (lastObject && lastObject.type === 'root') {
objectPath.pop();
}

View File

@ -41,7 +41,7 @@
:key="openmct.objects.makeKeyString(objectResult.identifier)"
:result="objectResult"
@preview-changed="previewChanged"
@click.native="selectedResult"
@click="selectedResult"
/>
</div>
<div
@ -53,7 +53,7 @@
v-for="(annotationResult) in annotationResults"
:key="openmct.objects.makeKeyString(annotationResult.identifier)"
:result="annotationResult"
@click.native="selectedResult"
@click="selectedResult"
/>
</div>
<div

View File

@ -7,10 +7,10 @@
<div class="c-labeled-input__label">{{ options.label }}</div>
</label>
<input
v-bind="options.attrs"
:id="uid"
:type="options.type"
:value="options.value"
v-bind="options.attrs"
>
</div>
</template>

View File

@ -128,7 +128,7 @@ export function simulateKeyEvent(opts) {
}
function clearBuiltinSpy(funcDefinition) {
funcDefinition.object[funcDefinition.functionName] = funcDefinition.nativeFunction;
funcDefinition.object[funcDefinition.functionName] = funcDefinitionFunction;
}
export function getLatestTelemetry(telemetry = [], opts = {}) {

View File

@ -37,7 +37,7 @@ const config = {
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
espressoTheme: './src/plugins/themes/espresso-theme.scss',
snowTheme: './src/plugins/themes/snow-theme.scss',
snowTheme: './src/plugins/themes/snow-theme.scss'
},
output: {
globalObject: 'this',
@ -65,7 +65,8 @@ const config = {
"MCT": path.join(__dirname, "src/MCT"),
"testUtils": path.join(__dirname, "src/utils/testUtils.js"),
"objectUtils": path.join(__dirname, "src/api/objects/object-utils.js"),
"utils": path.join(__dirname, "src/utils")
"utils": path.join(__dirname, "src/utils"),
"vue": "@vue/compat"
}
},
plugins: [
@ -119,7 +120,15 @@ const config = {
},
{
test: /\.vue$/,
use: 'vue-loader'
loader: 'vue-loader',
options: {
hotReload: false,
compilerOptions: {
compatConfig: {
MODE: 3
}
}
}
},
{
test: /\.html$/,
@ -148,6 +157,9 @@ const config = {
}
]
},
externals: {
"vue": "Vue"
},
stats: 'errors-warnings',
performance: {
// We should eventually consider chunking to decrease

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