Compare commits
66 Commits
misc-ui-48
...
vue-3-migr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1258b89d39 | ||
|
|
034337aed3 | ||
|
|
f3376ad861 | ||
|
|
59a14c91fd | ||
|
|
03833fbdee | ||
|
|
3214ceb80c | ||
|
|
d638314cbd | ||
|
|
b74b27c464 | ||
|
|
d35e161701 | ||
|
|
653cb62f9c | ||
|
|
19b3232fa0 | ||
|
|
19892aab53 | ||
|
|
a168ce25cf | ||
|
|
189c58f952 | ||
|
|
0dfc028e1b | ||
|
|
77e93f1aee | ||
|
|
394fbbe61b | ||
|
|
40afb04f0c | ||
|
|
be73b0158a | ||
|
|
625205f24b | ||
|
|
a706a8b73e | ||
|
|
1ddf5e5137 | ||
|
|
a79646a915 | ||
|
|
d5266e7ac7 | ||
|
|
05de7ee2e0 | ||
|
|
d599084816 | ||
|
|
dad88112c4 | ||
|
|
1efe9b56b1 | ||
|
|
5ea92674bd | ||
|
|
50996d3a54 | ||
|
|
db300d861c | ||
|
|
ac809b6e13 | ||
|
|
202d6d8c5d | ||
|
|
e70bcc414c | ||
|
|
7bb4a136d7 | ||
|
|
8af3b4309f | ||
|
|
bed3d83fd7 | ||
|
|
efda42cf6d | ||
|
|
e8ee5b3fc9 | ||
|
|
393cb9767f | ||
|
|
8b5daad65c | ||
|
|
fabfecdb3e | ||
|
|
a2d8b13204 | ||
|
|
4b14d2d6d2 | ||
|
|
d545124942 | ||
|
|
6abdbfdff0 | ||
|
|
500e655476 | ||
|
|
5e1f026db2 | ||
|
|
d9efae98c8 | ||
|
|
091f6406a8 | ||
|
|
42a0e503cc | ||
|
|
4697352f60 | ||
|
|
015c764ab3 | ||
|
|
8fe465d9fc | ||
|
|
9c1368885a | ||
|
|
391c0b2e7c | ||
|
|
2ae061dbcd | ||
|
|
41fc502564 | ||
|
|
b4554d2fc1 | ||
|
|
feba5f6d3b | ||
|
|
4357d35f4a | ||
|
|
5041f80e5b | ||
|
|
9e23f79bc8 | ||
|
|
bd1e869f6a | ||
|
|
e4a36532e7 | ||
|
|
2bc2316613 |
14
.github/dependabot.yml
vendored
@@ -13,11 +13,17 @@ updates:
|
||||
- "pr:daveit"
|
||||
- "pr:platform"
|
||||
ignore:
|
||||
- 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.
|
||||
#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"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "eslint-plugin-vue" #Lots of noise in these type patch releases.
|
||||
- dependency-name: "eslint-plugin-vue"
|
||||
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: "/"
|
||||
|
||||
8
.github/workflows/npm-prerelease.yml
vendored
@@ -16,7 +16,11 @@ jobs:
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
- run: |
|
||||
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
|
||||
npm whoami
|
||||
npm publish --access=public --tag unstable openmct
|
||||
# - run: npm test
|
||||
|
||||
publish-npm-prerelease:
|
||||
needs: build
|
||||
@@ -28,6 +32,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 }}
|
||||
|
||||
@@ -21,4 +21,10 @@
|
||||
!copyright-notice.html
|
||||
!index.html
|
||||
!openmct.js
|
||||
!SECURITY.md
|
||||
!SECURITY.md
|
||||
|
||||
# Add e2e tests to npm package
|
||||
!/e2e/**/*
|
||||
|
||||
# ... except our test-data folder files.
|
||||
/e2e/test-data/*.json
|
||||
|
||||
@@ -10,7 +10,7 @@ accept changes from external contributors.
|
||||
|
||||
The short version:
|
||||
|
||||
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)
|
||||
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).
|
||||
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
|
||||
|
||||
@@ -6,10 +6,8 @@ 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!
|
||||
|
||||
## See Open MCT in Action
|
||||

|
||||
|
||||
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).
|
||||

|
||||
|
||||
## Building and Running Open MCT Locally
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
*/
|
||||
|
||||
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
|
||||
@@ -56,6 +57,10 @@ const Buffer = require('buffer').Buffer;
|
||||
* @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
|
||||
@@ -67,13 +72,18 @@ 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:text("${type}")`);
|
||||
await page.click(`li[role='menuitem']:text("${type}")`);
|
||||
|
||||
// Modify the name input field of the domain object to accept 'name'
|
||||
if (name) {
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
await nameInput.fill("");
|
||||
await nameInput.fill(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);
|
||||
}
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
@@ -96,8 +106,8 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
||||
}
|
||||
|
||||
return {
|
||||
name: name || `Unnamed ${type}`,
|
||||
uuid: uuid,
|
||||
name,
|
||||
uuid,
|
||||
url: objectUrl
|
||||
};
|
||||
}
|
||||
@@ -225,15 +235,14 @@ async function getHashUrlToDomainObject(page, uuid) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
|
||||
* Utilizes the OpenMCT API to detect if the UI is in Edit mode.
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
|
||||
* @return {Promise<boolean>} true if the object has an active transaction, false otherwise
|
||||
* @return {Promise<boolean>} true if the Open MCT is in Edit Mode
|
||||
*/
|
||||
async function _isInEditMode(page, identifier) {
|
||||
// eslint-disable-next-line no-return-await
|
||||
return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
|
||||
return await page.evaluate(() => window.openmct.editor.isEditing());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { createDomainObjectWithDefaults } = require('../appActions');
|
||||
|
||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
||||
|
||||
/**
|
||||
@@ -38,24 +40,17 @@ async function enterTextEntry(page, text) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
|
||||
@@ -126,13 +126,21 @@ 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) => {
|
||||
page: async ({ page, theme }, use, testInfo) => {
|
||||
// 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 }],
|
||||
@@ -140,22 +148,5 @@ 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;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { test, expect } = require('../../pluginFixtures.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('Timer Foo');
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name);
|
||||
await page.goto(timer2.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Bar');
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name);
|
||||
await page.goto(timer3.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name);
|
||||
});
|
||||
|
||||
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('Folder Foo');
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name);
|
||||
await page.goto(folder2.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Bar');
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name);
|
||||
await page.goto(folder3.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Baz');
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name);
|
||||
|
||||
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
|
||||
expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
*/
|
||||
|
||||
// Structure: Some standard Imports. Please update the required pathing.
|
||||
const { test, expect } = require('../../baseFixtures');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
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('text=OK').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
}
|
||||
|
||||
@@ -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:has-text("Sine Wave Generator")').click();
|
||||
await page.locator('li[role="menuitem"]: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('text=OK').click(),
|
||||
page.locator('button:has-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('Unnamed Overlay Plot');
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
|
||||
test.describe("CouchDB Status Indicator @couchdb", () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
|
||||
@@ -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('../../../baseFixtures');
|
||||
const { test, expect } = require('../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../appActions');
|
||||
|
||||
test.describe('Example Event Generator CRUD Operations', () => {
|
||||
|
||||
@@ -96,7 +96,7 @@ test.describe('Sine Wave Generator', () => {
|
||||
//Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('text=OK')
|
||||
page.click('button:has-text("OK")')
|
||||
]);
|
||||
|
||||
// Verify that the Sine Wave Generator is displayed and correct
|
||||
|
||||
@@ -25,6 +25,7 @@ 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';
|
||||
@@ -43,7 +44,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('text=OK')).toBeDisabled();
|
||||
await expect(page.locator('button:has-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'
|
||||
@@ -52,13 +53,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('text=OK')).toBeEnabled();
|
||||
await expect(page.locator('button:has-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('text=OK')
|
||||
page.click('button:has-text("OK")')
|
||||
]);
|
||||
|
||||
//Verify that the Domain Object has been created with the corrected title property
|
||||
@@ -91,6 +92,44 @@ 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}) => {});
|
||||
|
||||
@@ -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('text=OK').click();
|
||||
await page.locator('button:has-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:has-text("Telemetry Table")').click();
|
||||
await page.locator('li[role="menuitem"]: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('text=OK').click();
|
||||
await page.locator('button:has-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:has-text("Folder")').click();
|
||||
await page.locator('li[role="menuitem"]: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('text=OK').click();
|
||||
await page.locator('button:has-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('text=OK').click();
|
||||
await page.locator('button:has-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();
|
||||
|
||||
@@ -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:has-text("Condition Set")').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Condition Set")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('text=OK')
|
||||
page.click('button:has-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 text=Remove
|
||||
await page.locator('text=Remove').click();
|
||||
await page.locator('text=OK').click();
|
||||
// Click 'Remove' and press OK
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-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();
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
|
||||
|
||||
test.describe('Testing Display Layout @unstable', () => {
|
||||
test.describe('Display Layout', () => {
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
@@ -55,12 +55,12 @@ test.describe('Testing Display Layout @unstable', () => {
|
||||
// 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 = await getTelemValuePromise;
|
||||
const formattedTelemetryValue = getTelemValuePromise;
|
||||
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
|
||||
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||
|
||||
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
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('Testing Display Layout @unstable', () => {
|
||||
|
||||
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const formattedTelemetryValue = await getTelemValuePromise;
|
||||
const formattedTelemetryValue = getTelemValuePromise;
|
||||
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
|
||||
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||
|
||||
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
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,16 +116,20 @@ test.describe('Testing Display Layout @unstable', () => {
|
||||
|
||||
// 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('text=Remove').click();
|
||||
await page.locator('text=OK').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
// delete
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
expect(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
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: "Test Display Layout"
|
||||
});
|
||||
@@ -144,18 +148,18 @@ test.describe('Testing Display Layout @unstable', () => {
|
||||
// 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();
|
||||
|
||||
// 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();
|
||||
// Go to the original Sine Wave Generator to navigate away from the Display 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('text=Remove').click();
|
||||
await page.locator('text=OK').click();
|
||||
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.locator('.c-tree__item .c-tree__item__name:text("Test Display Layout")').click();
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,12 +23,13 @@
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Testing Flexible Layout @unstable', () => {
|
||||
test.describe('Flexible Layout', () => {
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create Sine Wave Generator
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: "Test Sine Wave Generator"
|
||||
});
|
||||
@@ -54,13 +55,81 @@ test.describe('Testing Flexible Layout @unstable', () => {
|
||||
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 = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
|
||||
let dragWrapper = 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 = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
|
||||
dragWrapper = 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);
|
||||
});
|
||||
});
|
||||
|
||||
124
e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/*****************************************************************************
|
||||
* 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
|
||||
});
|
||||
});
|
||||
@@ -40,10 +40,10 @@ test.describe('Example Imagery Object', () => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a default 'Example Imagery' object
|
||||
await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
|
||||
const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
|
||||
|
||||
// Verify that the created object is focused
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
|
||||
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('text=Example Imagery');
|
||||
await page.click('li[role="menuitem"]:has-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('text=OK'),
|
||||
page.click('button:has-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('text=Example Imagery');
|
||||
await page.click('li[role="menuitem"]:has-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('text=OK'),
|
||||
page.click('button:has-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('text=Example Imagery');
|
||||
await page.click('li[role="menuitem"]:has-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('text=OK'),
|
||||
page.click('button:has-text("OK")'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -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('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const nbUtils = require('../../../../helper/notebookUtils');
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
|
||||
@@ -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(`Unnamed ${CUSTOM_NAME}`);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`${notebook.name}`);
|
||||
});
|
||||
|
||||
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
|
||||
test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
|
||||
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("Unnamed ${CUSTOM_NAME}")`);
|
||||
const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`);
|
||||
|
||||
// notebook tree object exists
|
||||
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
|
||||
|
||||
// Click Remove Text
|
||||
await page.locator('text=Remove').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
|
||||
// Click 'OK' on confirmation window and wait for save banner to appear
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.locator('button:has-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('text=Ok').click()
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
// deleted page, should no longer exist
|
||||
@@ -145,10 +145,9 @@ 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, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
await nbUtils.dragAndDropEmbed(page, myItemsFolderName);
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const notebook = await startAndAddRestrictedNotebookObject(page);
|
||||
await nbUtils.dragAndDropEmbed(page, notebook);
|
||||
});
|
||||
|
||||
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
|
||||
|
||||
@@ -36,7 +36,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Create an entry
|
||||
@@ -45,6 +45,8 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`Entry ${iteration}`);
|
||||
}
|
||||
|
||||
return notebook;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +55,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) {
|
||||
await createNotebookAndEntry(page, iterations);
|
||||
const notebook = await createNotebookAndEntry(page, iterations);
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Hover and click "Add Tag" button
|
||||
@@ -75,6 +77,8 @@ 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', () => {
|
||||
@@ -173,10 +177,10 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
|
||||
const ITERATIONS = 4;
|
||||
await createNotebookEntryAndTags(page, ITERATIONS);
|
||||
const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
@@ -189,11 +193,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
page.goto('./#/browse/mine?hideTree=false'),
|
||||
page.click('.c-disclosure-triangle')
|
||||
]);
|
||||
// Click Unnamed Clock
|
||||
await page.click('text="Unnamed Clock"');
|
||||
// Click Clock
|
||||
await page.click(`text=${clock.name}`);
|
||||
|
||||
// Click Unnamed Notebook
|
||||
await page.click('text="Unnamed Notebook"');
|
||||
// Click Notebook
|
||||
await page.click(`text=${notebook.name}`);
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
@@ -207,14 +211,13 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
// Click Unnamed Notebook
|
||||
await page.click('text="Unnamed Notebook"');
|
||||
// Click Notebook
|
||||
await page.click(`text="${notebook.name}"`);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:has-text("Overlay Plot")').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.locator('button:has-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:has-text("Sine Wave Generator")').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 21 KiB |
@@ -88,10 +88,10 @@ async function makeOverlayPlot(page, myItemsFolderName) {
|
||||
// create overlay plot
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
page.locator('button:has-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:has-text("Sine Wave Generator")').click();
|
||||
await page.locator('li[role="menuitem"]: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('text=OK').click(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -88,11 +88,11 @@ async function makeStackedPlot(page, myItemsFolderName) {
|
||||
|
||||
// create stacked plot
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Stacked Plot")').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
page.locator('button:has-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:has-text("Sine Wave Generator")').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -68,10 +68,10 @@ async function makeOverlayPlot(page) {
|
||||
// create overlay plot
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
page.locator('button:has-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:has-text("Sine Wave Generator")').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
|
||||
// Click OK to make generator
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Plot Integrity Testing @unstable', () => {
|
||||
|
||||
93
e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/*****************************************************************************
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
|
||||
|
||||
test.describe('Time conductor operations', () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ test.describe('Timer', () => {
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||
});
|
||||
|
||||
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
|
||||
test('Can perform actions on the Timer', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4313'
|
||||
|
||||
@@ -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;
|
||||
|
||||
await createObjectsForSearch(page, myItemsFolderName);
|
||||
const createdObjects = await createObjectsForSearch(page);
|
||||
|
||||
// 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 text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
// Click the Elements pool to dismiss the search menu
|
||||
await page.locator('.l-pane__label:has-text("Elements")').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('Unnamed Display Layout');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(createdObjects.displayLayout.name);
|
||||
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('text=OK').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
}
|
||||
|
||||
async function waitForSearchCompletion(page) {
|
||||
@@ -197,75 +197,56 @@ async function waitForSearchCompletion(page) {
|
||||
* Creates some domain objects for searching
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createObjectsForSearch(page, myItemsFolderName) {
|
||||
async function createObjectsForSearch(page) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
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 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=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 blueFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Blue Folder',
|
||||
parent: redFolder.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 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 B'),
|
||||
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 C'),
|
||||
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 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()
|
||||
]);
|
||||
return {
|
||||
redFolder,
|
||||
blueFolder,
|
||||
clockA,
|
||||
clockB,
|
||||
clockC,
|
||||
clockD,
|
||||
displayLayout
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ test.describe('Performance tests', () => {
|
||||
await page.setInputFiles('#fileElem', filePath);
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
</body>
|
||||
<script>
|
||||
const THIRTY_SECONDS = 30 * 1000;
|
||||
@@ -75,7 +76,8 @@
|
||||
const TWO_HOURS = ONE_HOUR * 2;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
// openmct.install(openmct.plugins.LocalStorage());
|
||||
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
|
||||
|
||||
openmct.install(openmct.plugins.example.Generator());
|
||||
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
|
||||
|
||||
45
openmct.js
@@ -30,8 +30,53 @@ if (document.currentScript) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} BuildInfo
|
||||
* @property {string} version
|
||||
* @property {string} buildDate
|
||||
* @property {string} revision
|
||||
* @property {string} branch
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} OpenMCT
|
||||
* @property {BuildInfo} buildInfo
|
||||
* @property {*} selection
|
||||
* @property {import('./src/api/time/TimeAPI').default} time
|
||||
* @property {import('./src/api/composition/CompositionAPI').default} composition
|
||||
* @property {*} objectViews
|
||||
* @property {*} inspectorViews
|
||||
* @property {*} propertyEditors
|
||||
* @property {*} toolbars
|
||||
* @property {*} types
|
||||
* @property {import('./src/api/objects/ObjectAPI').default} objects
|
||||
* @property {import('./src/api/telemetry/TelemetryAPI').default} telemetry
|
||||
* @property {import('./src/api/indicators/IndicatorAPI').default} indicators
|
||||
* @property {import('./src/api/user/UserAPI').default} user
|
||||
* @property {import('./src/api/notifications/NotificationAPI').default} notifications
|
||||
* @property {import('./src/api/Editor').default} editor
|
||||
* @property {import('./src/api/overlays/OverlayAPI')} overlays
|
||||
* @property {import('./src/api/menu/MenuAPI').default} menus
|
||||
* @property {import('./src/api/actions/ActionsAPI').default} actions
|
||||
* @property {import('./src/api/status/StatusAPI').default} status
|
||||
* @property {*} priority
|
||||
* @property {import('./src/ui/router/ApplicationRouter')} router
|
||||
* @property {import('./src/api/faultmanagement/FaultManagementAPI').default} faults
|
||||
* @property {import('./src/api/forms/FormsAPI').default} forms
|
||||
* @property {import('./src/api/Branding').default} branding
|
||||
* @property {import('./src/api/annotation/AnnotationAPI').default} annotation
|
||||
* @property {{(plugin: OpenMCTPlugin) => void}} install
|
||||
* @property {{() => string}} getAssetPath
|
||||
* @property {{(domElement: HTMLElement, isHeadlessMode: boolean) => void}} start
|
||||
* @property {{() => void}} startHeadless
|
||||
* @property {{() => void}} destroy
|
||||
* @property {OpenMCTPlugin[]} plugins
|
||||
* @property {OpenMCTComponent[]} components
|
||||
*/
|
||||
|
||||
const MCT = require('./src/MCT');
|
||||
|
||||
/** @type {OpenMCT} */
|
||||
const openmct = new MCT();
|
||||
|
||||
module.exports = openmct;
|
||||
|
||||
41
package.json
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.2",
|
||||
"version": "2.1.4-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.11.0",
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@percy/cli": "1.16.0",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.25.2",
|
||||
"@types/jasmine": "4.0.1",
|
||||
"@types/lodash": "4.14.178",
|
||||
"babel-loader": "8.2.5",
|
||||
"@types/jasmine": "4.3.1",
|
||||
"@types/lodash": "4.14.191",
|
||||
"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.25.0",
|
||||
"eslint": "8.29.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.11.2",
|
||||
"eslint-plugin-vue": "9.6.0",
|
||||
"eslint-plugin-vue": "9.8.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.4.0",
|
||||
"jasmine-core": "4.5.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.6.1",
|
||||
"mini-css-extract-plugin": "2.7.2",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.37",
|
||||
"moment-timezone": "0.5.38",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"playwright-core": "1.25.2",
|
||||
@@ -53,17 +53,19 @@
|
||||
"plotly.js-gl2d-dist": "2.14.0",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.55.0",
|
||||
"sass": "1.56.1",
|
||||
"sass-loader": "13.0.2",
|
||||
"sinon": "14.0.0",
|
||||
"sinon": "14.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "4.9.3",
|
||||
"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": "4.10.0",
|
||||
"webpack-cli": "5.0.0",
|
||||
"webpack-dev-server": "4.11.1",
|
||||
"webpack-merge": "5.8.0"
|
||||
},
|
||||
@@ -96,7 +98,7 @@
|
||||
"cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
|
||||
"cov:e2e:stable:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
|
||||
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
|
||||
"prepare": "npm run build:prod"
|
||||
"prepare": "npm run build:prod && npx tsc"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -113,6 +115,5 @@
|
||||
"ios_saf > 15"
|
||||
],
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"private": true
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
||||
19
src/MCT.js
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
define([
|
||||
'EventEmitter',
|
||||
'./api/api',
|
||||
@@ -81,13 +81,11 @@ define([
|
||||
/**
|
||||
* The Open MCT application. This may be configured by installing plugins
|
||||
* or registering extensions before the application is started.
|
||||
* @class MCT
|
||||
* @constructor
|
||||
* @memberof module:openmct
|
||||
* @augments {EventEmitter}
|
||||
*/
|
||||
function MCT() {
|
||||
EventEmitter.call(this);
|
||||
/* eslint-disable no-undef */
|
||||
this.buildInfo = {
|
||||
version: __OPENMCT_VERSION__,
|
||||
buildDate: __OPENMCT_BUILD_DATE__,
|
||||
@@ -101,7 +99,7 @@ define([
|
||||
* Tracks current selection state of the application.
|
||||
* @private
|
||||
*/
|
||||
['selection', () => new Selection(this)],
|
||||
['selection', () => new Selection.default(this)],
|
||||
|
||||
/**
|
||||
* MCT's time conductor, which may be used to synchronize view contents
|
||||
@@ -125,7 +123,7 @@ define([
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name composition
|
||||
*/
|
||||
['composition', () => new api.CompositionAPI(this)],
|
||||
['composition', () => new api.CompositionAPI.default(this)],
|
||||
|
||||
/**
|
||||
* Registry for views of domain objects which should appear in the
|
||||
@@ -330,7 +328,7 @@ define([
|
||||
* @param {HTMLElement} [domElement] the DOM element in which to run
|
||||
* MCT; if undefined, MCT will be run in the body of the document
|
||||
*/
|
||||
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
|
||||
MCT.prototype.start = function (domElement = document.body.firstElementChild, isHeadlessMode = false) {
|
||||
if (this.types.get('layout') === undefined) {
|
||||
this.install(this.plugins.DisplayLayout({
|
||||
showAsView: ['summary-widget']
|
||||
@@ -351,7 +349,7 @@ define([
|
||||
*/
|
||||
|
||||
if (!isHeadlessMode) {
|
||||
const appLayout = new Vue({
|
||||
const appLayout = Vue.createApp({
|
||||
components: {
|
||||
'Layout': Layout.default
|
||||
},
|
||||
@@ -360,9 +358,8 @@ define([
|
||||
},
|
||||
template: '<Layout ref="layout"></Layout>'
|
||||
});
|
||||
domElement.appendChild(appLayout.$mount().$el);
|
||||
|
||||
this.layout = appLayout.$refs.layout;
|
||||
appLayout.mount(domElement);
|
||||
this.layout = appLayout._instance.refs.layout;
|
||||
Browse(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
let brandingOptions = {};
|
||||
|
||||
/**
|
||||
* @typedef {Object} BrandingOptions
|
||||
* @memberOf openmct/branding
|
||||
* @typedef {object} BrandingOptions
|
||||
* @property {string} smallLogoImage URL to the image to use as the applications logo.
|
||||
* This logo will appear on every screen and when clicked will launch the about dialog.
|
||||
* @property {string} aboutHtml Custom content for the about screen. When defined the
|
||||
|
||||
@@ -56,17 +56,12 @@ export default class Editor extends EventEmitter {
|
||||
* Save any unsaved changes from this editing session. This will
|
||||
* end the current transaction.
|
||||
*/
|
||||
save() {
|
||||
async save() {
|
||||
const transaction = this.openmct.objects.getActiveTransaction();
|
||||
|
||||
return transaction.commit()
|
||||
.then(() => {
|
||||
this.editing = false;
|
||||
this.emit('isEditing', false);
|
||||
this.openmct.objects.endTransaction();
|
||||
}).catch(error => {
|
||||
throw error;
|
||||
});
|
||||
await transaction.commit();
|
||||
this.editing = false;
|
||||
this.emit('isEditing', false);
|
||||
this.openmct.objects.endTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -94,7 +94,6 @@ describe("The Annotation API", () => {
|
||||
openmct.startHeadless();
|
||||
});
|
||||
afterEach(async () => {
|
||||
openmct.objects.providers = {};
|
||||
await resetApplicationState(openmct);
|
||||
});
|
||||
it("is defined", () => {
|
||||
|
||||
@@ -37,7 +37,9 @@ define([
|
||||
'./types/TypeRegistry',
|
||||
'./user/UserAPI',
|
||||
'./annotation/AnnotationAPI'
|
||||
], function (
|
||||
],
|
||||
|
||||
function (
|
||||
ActionsAPI,
|
||||
CompositionAPI,
|
||||
EditorAPI,
|
||||
|
||||
@@ -20,34 +20,41 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'lodash',
|
||||
'EventEmitter',
|
||||
'./DefaultCompositionProvider',
|
||||
'./CompositionCollection'
|
||||
], function (
|
||||
_,
|
||||
EventEmitter,
|
||||
DefaultCompositionProvider,
|
||||
CompositionCollection
|
||||
) {
|
||||
import DefaultCompositionProvider from './DefaultCompositionProvider';
|
||||
import CompositionCollection from './CompositionCollection';
|
||||
|
||||
/**
|
||||
* @typedef {import('./CompositionProvider').default} CompositionProvider
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||
*/
|
||||
|
||||
/**
|
||||
* An interface for interacting with the composition of domain objects.
|
||||
* The composition of a domain object is the list of other domain objects
|
||||
* it "contains" (for instance, that should be displayed beneath it
|
||||
* in the tree.)
|
||||
* @constructor
|
||||
*/
|
||||
export default class CompositionAPI {
|
||||
/**
|
||||
* An interface for interacting with the composition of domain objects.
|
||||
* The composition of a domain object is the list of other domain objects
|
||||
* it "contains" (for instance, that should be displayed beneath it
|
||||
* in the tree.)
|
||||
*
|
||||
* @interface CompositionAPI
|
||||
* @returns {module:openmct.CompositionCollection}
|
||||
* @memberof module:openmct
|
||||
* @param {OpenMCT} publicAPI
|
||||
*/
|
||||
function CompositionAPI(publicAPI) {
|
||||
constructor(publicAPI) {
|
||||
/** @type {CompositionProvider[]} */
|
||||
this.registry = [];
|
||||
/** @type {CompositionPolicy[]} */
|
||||
this.policies = [];
|
||||
this.addProvider(new DefaultCompositionProvider(publicAPI, this));
|
||||
/** @type {OpenMCT} */
|
||||
this.publicAPI = publicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a composition provider.
|
||||
*
|
||||
@@ -55,21 +62,19 @@ define([
|
||||
* behavior for certain domain objects.
|
||||
*
|
||||
* @method addProvider
|
||||
* @param {module:openmct.CompositionProvider} provider the provider to add
|
||||
* @memberof module:openmct.CompositionAPI#
|
||||
* @param {CompositionProvider} provider the provider to add
|
||||
*/
|
||||
CompositionAPI.prototype.addProvider = function (provider) {
|
||||
addProvider(provider) {
|
||||
this.registry.unshift(provider);
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Retrieve the composition (if any) of this domain object.
|
||||
*
|
||||
* @method get
|
||||
* @returns {module:openmct.CompositionCollection}
|
||||
* @memberof module:openmct.CompositionAPI#
|
||||
* @param {DomainObject} domainObject
|
||||
* @returns {CompositionCollection}
|
||||
*/
|
||||
CompositionAPI.prototype.get = function (domainObject) {
|
||||
get(domainObject) {
|
||||
const provider = this.registry.find(p => {
|
||||
return p.appliesTo(domainObject);
|
||||
});
|
||||
@@ -79,8 +84,7 @@ define([
|
||||
}
|
||||
|
||||
return new CompositionCollection(domainObject, provider, this.publicAPI);
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* A composition policy is a function which either allows or disallows
|
||||
* placing one object in another's composition.
|
||||
@@ -90,52 +94,51 @@ define([
|
||||
* generally be written to return true in the default case.
|
||||
*
|
||||
* @callback CompositionPolicy
|
||||
* @memberof module:openmct.CompositionAPI~
|
||||
* @param {module:openmct.DomainObject} containingObject the object which
|
||||
* @param {DomainObject} containingObject the object which
|
||||
* would act as a container
|
||||
* @param {module:openmct.DomainObject} containedObject the object which
|
||||
* @param {DomainObject} containedObject the object which
|
||||
* would be contained
|
||||
* @returns {boolean} false if this composition should be disallowed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add a composition policy. Composition policies may disallow domain
|
||||
* objects from containing other domain objects.
|
||||
*
|
||||
* @method addPolicy
|
||||
* @param {module:openmct.CompositionAPI~CompositionPolicy} policy
|
||||
* @param {CompositionPolicy} policy
|
||||
* the policy to add
|
||||
* @memberof module:openmct.CompositionAPI#
|
||||
*/
|
||||
CompositionAPI.prototype.addPolicy = function (policy) {
|
||||
addPolicy(policy) {
|
||||
this.policies.push(policy);
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Check whether or not a domain object is allowed to contain another
|
||||
* domain object.
|
||||
*
|
||||
* @private
|
||||
* @method checkPolicy
|
||||
* @param {module:openmct.DomainObject} containingObject the object which
|
||||
* @param {DomainObject} container the object which
|
||||
* would act as a container
|
||||
* @param {module:openmct.DomainObject} containedObject the object which
|
||||
* @param {DomainObject} containee the object which
|
||||
* would be contained
|
||||
* @returns {boolean} false if this composition should be disallowed
|
||||
|
||||
* @param {module:openmct.CompositionAPI~CompositionPolicy} policy
|
||||
* @param {CompositionPolicy} policy
|
||||
* the policy to add
|
||||
* @memberof module:openmct.CompositionAPI#
|
||||
*/
|
||||
CompositionAPI.prototype.checkPolicy = function (container, containee) {
|
||||
checkPolicy(container, containee) {
|
||||
return this.policies.every(function (policy) {
|
||||
return policy(container, containee);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
CompositionAPI.prototype.supportsComposition = function (domainObject) {
|
||||
/**
|
||||
* Check whether or not a domainObject supports composition
|
||||
*
|
||||
* @param {DomainObject} domainObject
|
||||
* @returns {boolean} true if the domainObject supports composition
|
||||
*/
|
||||
supportsComposition(domainObject) {
|
||||
return this.get(domainObject) !== undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return CompositionAPI;
|
||||
});
|
||||
|
||||
@@ -1,325 +1,319 @@
|
||||
define([
|
||||
'./CompositionAPI',
|
||||
'./CompositionCollection'
|
||||
], function (
|
||||
CompositionAPI,
|
||||
CompositionCollection
|
||||
) {
|
||||
import CompositionAPI from './CompositionAPI';
|
||||
import CompositionCollection from './CompositionCollection';
|
||||
|
||||
describe('The Composition API', function () {
|
||||
let publicAPI;
|
||||
let compositionAPI;
|
||||
let topicService;
|
||||
let mutationTopic;
|
||||
describe('The Composition API', function () {
|
||||
let publicAPI;
|
||||
let compositionAPI;
|
||||
let topicService;
|
||||
let mutationTopic;
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
mutationTopic = jasmine.createSpyObj('mutationTopic', [
|
||||
'listen'
|
||||
]);
|
||||
topicService = jasmine.createSpy('topicService');
|
||||
topicService.and.returnValue(mutationTopic);
|
||||
publicAPI = {};
|
||||
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
|
||||
'get',
|
||||
'mutate',
|
||||
'observe',
|
||||
'areIdsEqual'
|
||||
]);
|
||||
|
||||
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
|
||||
return id1.namespace === id2.namespace && id1.key === id2.key;
|
||||
});
|
||||
|
||||
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
|
||||
'checkPolicy'
|
||||
]);
|
||||
publicAPI.composition.checkPolicy.and.returnValue(true);
|
||||
|
||||
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
|
||||
'on'
|
||||
]);
|
||||
publicAPI.objects.get.and.callFake(function (identifier) {
|
||||
return Promise.resolve({identifier: identifier});
|
||||
});
|
||||
publicAPI.$injector = jasmine.createSpyObj('$injector', [
|
||||
'get'
|
||||
]);
|
||||
publicAPI.$injector.get.and.returnValue(topicService);
|
||||
compositionAPI = new CompositionAPI(publicAPI);
|
||||
});
|
||||
|
||||
it('returns falsy if an object does not support composition', function () {
|
||||
expect(compositionAPI.get({})).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('default composition', function () {
|
||||
let domainObject;
|
||||
let composition;
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
mutationTopic = jasmine.createSpyObj('mutationTopic', [
|
||||
'listen'
|
||||
]);
|
||||
topicService = jasmine.createSpy('topicService');
|
||||
topicService.and.returnValue(mutationTopic);
|
||||
publicAPI = {};
|
||||
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
|
||||
'get',
|
||||
'mutate',
|
||||
'observe',
|
||||
'areIdsEqual'
|
||||
]);
|
||||
|
||||
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
|
||||
return id1.namespace === id2.namespace && id1.key === id2.key;
|
||||
});
|
||||
|
||||
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
|
||||
'checkPolicy'
|
||||
]);
|
||||
publicAPI.composition.checkPolicy.and.returnValue(true);
|
||||
|
||||
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
|
||||
'on'
|
||||
]);
|
||||
publicAPI.objects.get.and.callFake(function (identifier) {
|
||||
return Promise.resolve({identifier: identifier});
|
||||
});
|
||||
publicAPI.$injector = jasmine.createSpyObj('$injector', [
|
||||
'get'
|
||||
]);
|
||||
publicAPI.$injector.get.and.returnValue(topicService);
|
||||
compositionAPI = new CompositionAPI(publicAPI);
|
||||
domainObject = {
|
||||
name: 'test folder',
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'a'
|
||||
},
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'b'
|
||||
},
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'c'
|
||||
}
|
||||
]
|
||||
};
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('returns falsy if an object does not support composition', function () {
|
||||
expect(compositionAPI.get({})).toBeFalsy();
|
||||
it('returns composition collection', function () {
|
||||
expect(composition).toBeDefined();
|
||||
expect(composition).toEqual(jasmine.any(CompositionCollection));
|
||||
});
|
||||
|
||||
describe('default composition', function () {
|
||||
let domainObject;
|
||||
let composition;
|
||||
it('correctly reflects composability', function () {
|
||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
|
||||
delete domainObject.composition;
|
||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
domainObject = {
|
||||
name: 'test folder',
|
||||
it('loads composition from domain object', function () {
|
||||
const listener = jasmine.createSpy('addListener');
|
||||
composition.on('add', listener);
|
||||
|
||||
return composition.load().then(function () {
|
||||
expect(listener.calls.count()).toBe(3);
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'a'
|
||||
},
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'b'
|
||||
},
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'c'
|
||||
}
|
||||
]
|
||||
};
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('returns composition collection', function () {
|
||||
expect(composition).toBeDefined();
|
||||
expect(composition).toEqual(jasmine.any(CompositionCollection));
|
||||
});
|
||||
|
||||
it('correctly reflects composability', function () {
|
||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
|
||||
delete domainObject.composition;
|
||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
|
||||
});
|
||||
|
||||
it('loads composition from domain object', function () {
|
||||
const listener = jasmine.createSpy('addListener');
|
||||
composition.on('add', listener);
|
||||
|
||||
return composition.load().then(function () {
|
||||
expect(listener.calls.count()).toBe(3);
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: 'a'
|
||||
}
|
||||
});
|
||||
key: 'a'
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('supports reordering of composition', function () {
|
||||
let listener;
|
||||
beforeEach(function () {
|
||||
listener = jasmine.createSpy('reorderListener');
|
||||
composition.on('reorder', listener);
|
||||
});
|
||||
describe('supports reordering of composition', function () {
|
||||
let listener;
|
||||
beforeEach(function () {
|
||||
listener = jasmine.createSpy('reorderListener');
|
||||
composition.on('reorder', listener);
|
||||
|
||||
return composition.load();
|
||||
});
|
||||
it('', function () {
|
||||
composition.reorder(1, 0);
|
||||
let newComposition =
|
||||
return composition.load();
|
||||
});
|
||||
it('', function () {
|
||||
composition.reorder(1, 0);
|
||||
let newComposition =
|
||||
publicAPI.objects.mutate.calls.mostRecent().args[2];
|
||||
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
||||
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
||||
|
||||
expect(reorderPlan.oldIndex).toBe(1);
|
||||
expect(reorderPlan.newIndex).toBe(0);
|
||||
expect(newComposition[0].key).toEqual('b');
|
||||
expect(newComposition[1].key).toEqual('a');
|
||||
expect(newComposition[2].key).toEqual('c');
|
||||
});
|
||||
it('', function () {
|
||||
composition.reorder(0, 2);
|
||||
let newComposition =
|
||||
expect(reorderPlan.oldIndex).toBe(1);
|
||||
expect(reorderPlan.newIndex).toBe(0);
|
||||
expect(newComposition[0].key).toEqual('b');
|
||||
expect(newComposition[1].key).toEqual('a');
|
||||
expect(newComposition[2].key).toEqual('c');
|
||||
});
|
||||
it('', function () {
|
||||
composition.reorder(0, 2);
|
||||
let newComposition =
|
||||
publicAPI.objects.mutate.calls.mostRecent().args[2];
|
||||
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
||||
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
||||
|
||||
expect(reorderPlan.oldIndex).toBe(0);
|
||||
expect(reorderPlan.newIndex).toBe(2);
|
||||
expect(newComposition[0].key).toEqual('b');
|
||||
expect(newComposition[1].key).toEqual('c');
|
||||
expect(newComposition[2].key).toEqual('a');
|
||||
expect(reorderPlan.oldIndex).toBe(0);
|
||||
expect(reorderPlan.newIndex).toBe(2);
|
||||
expect(newComposition[0].key).toEqual('b');
|
||||
expect(newComposition[1].key).toEqual('c');
|
||||
expect(newComposition[2].key).toEqual('a');
|
||||
});
|
||||
});
|
||||
it('supports adding an object to composition', function () {
|
||||
let addListener = jasmine.createSpy('addListener');
|
||||
let mockChildObject = {
|
||||
identifier: {
|
||||
key: 'mock-key',
|
||||
namespace: ''
|
||||
}
|
||||
};
|
||||
composition.on('add', addListener);
|
||||
composition.add(mockChildObject);
|
||||
|
||||
expect(domainObject.composition.length).toBe(4);
|
||||
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
|
||||
});
|
||||
});
|
||||
|
||||
describe('static custom composition', function () {
|
||||
let customProvider;
|
||||
let domainObject;
|
||||
let composition;
|
||||
|
||||
beforeEach(function () {
|
||||
// A simple custom provider, returns the same composition for
|
||||
// all objects of a given type.
|
||||
customProvider = {
|
||||
appliesTo: function (object) {
|
||||
return object.type === 'custom-object-type';
|
||||
},
|
||||
load: function (object) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
]);
|
||||
},
|
||||
add: jasmine.createSpy('add'),
|
||||
remove: jasmine.createSpy('remove')
|
||||
};
|
||||
domainObject = {
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
type: 'custom-object-type'
|
||||
};
|
||||
compositionAPI.addProvider(customProvider);
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('supports listening and loading', function () {
|
||||
const addListener = jasmine.createSpy('addListener');
|
||||
composition.on('add', addListener);
|
||||
|
||||
return composition.load().then(function (children) {
|
||||
let listenObject;
|
||||
const loadedObject = children[0];
|
||||
|
||||
expect(addListener).toHaveBeenCalled();
|
||||
|
||||
listenObject = addListener.calls.mostRecent().args[0];
|
||||
expect(listenObject).toEqual(loadedObject);
|
||||
expect(loadedObject).toEqual({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
});
|
||||
});
|
||||
it('supports adding an object to composition', function () {
|
||||
let addListener = jasmine.createSpy('addListener');
|
||||
let mockChildObject = {
|
||||
});
|
||||
describe('Calling add or remove', function () {
|
||||
let mockChildObject;
|
||||
|
||||
beforeEach(function () {
|
||||
mockChildObject = {
|
||||
identifier: {
|
||||
key: 'mock-key',
|
||||
namespace: ''
|
||||
}
|
||||
};
|
||||
composition.on('add', addListener);
|
||||
composition.add(mockChildObject);
|
||||
|
||||
expect(domainObject.composition.length).toBe(4);
|
||||
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
|
||||
});
|
||||
});
|
||||
|
||||
describe('static custom composition', function () {
|
||||
let customProvider;
|
||||
let domainObject;
|
||||
let composition;
|
||||
|
||||
beforeEach(function () {
|
||||
// A simple custom provider, returns the same composition for
|
||||
// all objects of a given type.
|
||||
customProvider = {
|
||||
appliesTo: function (object) {
|
||||
return object.type === 'custom-object-type';
|
||||
},
|
||||
load: function (object) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
]);
|
||||
},
|
||||
add: jasmine.createSpy('add'),
|
||||
remove: jasmine.createSpy('remove')
|
||||
};
|
||||
domainObject = {
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
type: 'custom-object-type'
|
||||
};
|
||||
compositionAPI.addProvider(customProvider);
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('supports listening and loading', function () {
|
||||
const addListener = jasmine.createSpy('addListener');
|
||||
composition.on('add', addListener);
|
||||
|
||||
return composition.load().then(function (children) {
|
||||
let listenObject;
|
||||
const loadedObject = children[0];
|
||||
|
||||
expect(addListener).toHaveBeenCalled();
|
||||
|
||||
listenObject = addListener.calls.mostRecent().args[0];
|
||||
expect(listenObject).toEqual(loadedObject);
|
||||
expect(loadedObject).toEqual({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Calling add or remove', function () {
|
||||
let mockChildObject;
|
||||
|
||||
beforeEach(function () {
|
||||
mockChildObject = {
|
||||
identifier: {
|
||||
key: 'mock-key',
|
||||
namespace: ''
|
||||
}
|
||||
};
|
||||
composition.add(mockChildObject);
|
||||
});
|
||||
|
||||
it('calls add on the provider', function () {
|
||||
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
||||
});
|
||||
|
||||
it('calls remove on the provider', function () {
|
||||
composition.remove(mockChildObject);
|
||||
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dynamic custom composition', function () {
|
||||
let customProvider;
|
||||
let domainObject;
|
||||
let composition;
|
||||
|
||||
beforeEach(function () {
|
||||
// A dynamic provider, loads an empty composition and exposes
|
||||
// listener functions.
|
||||
customProvider = jasmine.createSpyObj('dynamicProvider', [
|
||||
'appliesTo',
|
||||
'load',
|
||||
'on',
|
||||
'off'
|
||||
]);
|
||||
|
||||
customProvider.appliesTo.and.returnValue('true');
|
||||
customProvider.load.and.returnValue(Promise.resolve([]));
|
||||
|
||||
domainObject = {
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
type: 'custom-object-type'
|
||||
};
|
||||
compositionAPI.addProvider(customProvider);
|
||||
composition = compositionAPI.get(domainObject);
|
||||
it('calls add on the provider', function () {
|
||||
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
||||
});
|
||||
|
||||
it('supports listening and loading', function () {
|
||||
const addListener = jasmine.createSpy('addListener');
|
||||
const removeListener = jasmine.createSpy('removeListener');
|
||||
const addPromise = new Promise(function (resolve) {
|
||||
addListener.and.callFake(resolve);
|
||||
});
|
||||
const removePromise = new Promise(function (resolve) {
|
||||
removeListener.and.callFake(resolve);
|
||||
});
|
||||
|
||||
composition.on('add', addListener);
|
||||
composition.on('remove', removeListener);
|
||||
|
||||
expect(customProvider.on).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
'add',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(CompositionCollection)
|
||||
);
|
||||
expect(customProvider.on).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
'remove',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(CompositionCollection)
|
||||
);
|
||||
const add = customProvider.on.calls.all()[0].args[2];
|
||||
const remove = customProvider.on.calls.all()[1].args[2];
|
||||
|
||||
return composition.load()
|
||||
.then(function () {
|
||||
expect(addListener).not.toHaveBeenCalled();
|
||||
expect(removeListener).not.toHaveBeenCalled();
|
||||
add({
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
});
|
||||
|
||||
return addPromise;
|
||||
}).then(function () {
|
||||
expect(addListener).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
});
|
||||
remove(addListener.calls.mostRecent().args[0]);
|
||||
|
||||
return removePromise;
|
||||
}).then(function () {
|
||||
expect(removeListener).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
});
|
||||
});
|
||||
it('calls remove on the provider', function () {
|
||||
composition.remove(mockChildObject);
|
||||
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dynamic custom composition', function () {
|
||||
let customProvider;
|
||||
let domainObject;
|
||||
let composition;
|
||||
|
||||
beforeEach(function () {
|
||||
// A dynamic provider, loads an empty composition and exposes
|
||||
// listener functions.
|
||||
customProvider = jasmine.createSpyObj('dynamicProvider', [
|
||||
'appliesTo',
|
||||
'load',
|
||||
'on',
|
||||
'off'
|
||||
]);
|
||||
|
||||
customProvider.appliesTo.and.returnValue('true');
|
||||
customProvider.load.and.returnValue(Promise.resolve([]));
|
||||
|
||||
domainObject = {
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
type: 'custom-object-type'
|
||||
};
|
||||
compositionAPI.addProvider(customProvider);
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('supports listening and loading', function () {
|
||||
const addListener = jasmine.createSpy('addListener');
|
||||
const removeListener = jasmine.createSpy('removeListener');
|
||||
const addPromise = new Promise(function (resolve) {
|
||||
addListener.and.callFake(resolve);
|
||||
});
|
||||
const removePromise = new Promise(function (resolve) {
|
||||
removeListener.and.callFake(resolve);
|
||||
});
|
||||
|
||||
composition.on('add', addListener);
|
||||
composition.on('remove', removeListener);
|
||||
|
||||
expect(customProvider.on).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
'add',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(CompositionCollection)
|
||||
);
|
||||
expect(customProvider.on).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
'remove',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(CompositionCollection)
|
||||
);
|
||||
const add = customProvider.on.calls.all()[0].args[2];
|
||||
const remove = customProvider.on.calls.all()[1].args[2];
|
||||
|
||||
return composition.load()
|
||||
.then(function () {
|
||||
expect(addListener).not.toHaveBeenCalled();
|
||||
expect(removeListener).not.toHaveBeenCalled();
|
||||
add({
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
});
|
||||
|
||||
return addPromise;
|
||||
}).then(function () {
|
||||
expect(addListener).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
});
|
||||
remove(addListener.calls.mostRecent().args[0]);
|
||||
|
||||
return removePromise;
|
||||
}).then(function () {
|
||||
expect(removeListener).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,75 +20,98 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'lodash'
|
||||
], function (
|
||||
_
|
||||
) {
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./CompositionAPI').default} CompositionAPI
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ListenerMap
|
||||
* @property {Array.<any>} add
|
||||
* @property {Array.<any>} remove
|
||||
* @property {Array.<any>} load
|
||||
* @property {Array.<any>} reorder
|
||||
*/
|
||||
|
||||
/**
|
||||
* A CompositionCollection represents the list of domain objects contained
|
||||
* by another domain object. It provides methods for loading this
|
||||
* list asynchronously, modifying this list, and listening for changes to
|
||||
* this list.
|
||||
*
|
||||
* Usage:
|
||||
* ```javascript
|
||||
* var myViewComposition = MCT.composition.get(myViewObject);
|
||||
* myViewComposition.on('add', addObjectToView);
|
||||
* myViewComposition.on('remove', removeObjectFromView);
|
||||
* myViewComposition.load(); // will trigger `add` for all loaded objects.
|
||||
* ```
|
||||
*/
|
||||
export default class CompositionCollection {
|
||||
domainObject;
|
||||
#provider;
|
||||
#publicAPI;
|
||||
#listeners;
|
||||
#mutables;
|
||||
/**
|
||||
* A CompositionCollection represents the list of domain objects contained
|
||||
* by another domain object. It provides methods for loading this
|
||||
* list asynchronously, modifying this list, and listening for changes to
|
||||
* this list.
|
||||
*
|
||||
* Usage:
|
||||
* ```javascript
|
||||
* var myViewComposition = MCT.composition.get(myViewObject);
|
||||
* myViewComposition.on('add', addObjectToView);
|
||||
* myViewComposition.on('remove', removeObjectFromView);
|
||||
* myViewComposition.load(); // will trigger `add` for all loaded objects.
|
||||
* ```
|
||||
*
|
||||
* @interface CompositionCollection
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* @constructor
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* whose composition will be contained
|
||||
* @param {module:openmct.CompositionProvider} provider the provider
|
||||
* @param {import('./CompositionProvider').default} provider the provider
|
||||
* to use to retrieve other domain objects
|
||||
* @param {module:openmct.CompositionAPI} api the composition API, for
|
||||
* @param {OpenMCT} publicAPI the composition API, for
|
||||
* policy checks
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
function CompositionCollection(domainObject, provider, publicAPI) {
|
||||
constructor(domainObject, provider, publicAPI) {
|
||||
this.domainObject = domainObject;
|
||||
this.provider = provider;
|
||||
this.publicAPI = publicAPI;
|
||||
this.listeners = {
|
||||
/** @type {import('./CompositionProvider').default} */
|
||||
this.#provider = provider;
|
||||
/** @type {OpenMCT} */
|
||||
this.#publicAPI = publicAPI;
|
||||
/** @type {ListenerMap} */
|
||||
this.#listeners = {
|
||||
add: [],
|
||||
remove: [],
|
||||
load: [],
|
||||
reorder: []
|
||||
};
|
||||
this.onProviderAdd = this.onProviderAdd.bind(this);
|
||||
this.onProviderRemove = this.onProviderRemove.bind(this);
|
||||
this.mutables = {};
|
||||
this.onProviderAdd = this.#onProviderAdd.bind(this);
|
||||
this.onProviderRemove = this.#onProviderRemove.bind(this);
|
||||
this.#mutables = {};
|
||||
|
||||
if (this.domainObject.isMutable) {
|
||||
this.returnMutables = true;
|
||||
let unobserve = this.domainObject.$on('$_destroy', () => {
|
||||
Object.values(this.mutables).forEach(mutable => {
|
||||
this.publicAPI.objects.destroyMutable(mutable);
|
||||
Object.values(this.#mutables).forEach(mutable => {
|
||||
this.#publicAPI.objects.destroyMutable(mutable);
|
||||
});
|
||||
unobserve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for changes to this composition. Supports 'add', 'remove', and
|
||||
* 'load' events.
|
||||
*
|
||||
* @param event event to listen for, either 'add', 'remove' or 'load'.
|
||||
* @param callback to trigger when event occurs.
|
||||
* @param [context] context to use when invoking callback, optional.
|
||||
* @param {string} event event to listen for, either 'add', 'remove' or 'load'.
|
||||
* @param {(...args: any[]) => void} callback to trigger when event occurs.
|
||||
* @param {any} [context] to use when invoking callback, optional.
|
||||
*/
|
||||
CompositionCollection.prototype.on = function (event, callback, context) {
|
||||
if (!this.listeners[event]) {
|
||||
on(event, callback, context) {
|
||||
if (!this.#listeners[event]) {
|
||||
throw new Error('Event not supported by composition: ' + event);
|
||||
}
|
||||
|
||||
if (this.provider.on && this.provider.off) {
|
||||
if (this.#provider.on && this.#provider.off) {
|
||||
if (event === 'add') {
|
||||
this.provider.on(
|
||||
this.#provider.on(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
@@ -97,7 +120,7 @@ define([
|
||||
}
|
||||
|
||||
if (event === 'remove') {
|
||||
this.provider.on(
|
||||
this.#provider.on(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
@@ -106,36 +129,34 @@ define([
|
||||
}
|
||||
|
||||
if (event === 'reorder') {
|
||||
this.provider.on(
|
||||
this.#provider.on(
|
||||
this.domainObject,
|
||||
'reorder',
|
||||
this.onProviderReorder,
|
||||
this.#onProviderReorder,
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners[event].push({
|
||||
this.#listeners[event].push({
|
||||
callback: callback,
|
||||
context: context
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Remove a listener. Must be called with same exact parameters as
|
||||
* `off`.
|
||||
*
|
||||
* @param event
|
||||
* @param callback
|
||||
* @param [context]
|
||||
* @param {string} event
|
||||
* @param {(...args: any[]) => void} callback
|
||||
* @param {any} [context]
|
||||
*/
|
||||
|
||||
CompositionCollection.prototype.off = function (event, callback, context) {
|
||||
if (!this.listeners[event]) {
|
||||
off(event, callback, context) {
|
||||
if (!this.#listeners[event]) {
|
||||
throw new Error('Event not supported by composition: ' + event);
|
||||
}
|
||||
|
||||
const index = this.listeners[event].findIndex(l => {
|
||||
const index = this.#listeners[event].findIndex(l => {
|
||||
return l.callback === callback && l.context === context;
|
||||
});
|
||||
|
||||
@@ -143,125 +164,116 @@ define([
|
||||
throw new Error('Tried to remove a listener that does not exist');
|
||||
}
|
||||
|
||||
this.listeners[event].splice(index, 1);
|
||||
if (this.listeners[event].length === 0) {
|
||||
this.#listeners[event].splice(index, 1);
|
||||
if (this.#listeners[event].length === 0) {
|
||||
this._destroy();
|
||||
|
||||
// Remove provider listener if this is the last callback to
|
||||
// be removed.
|
||||
if (this.provider.off && this.provider.on) {
|
||||
if (this.#provider.off && this.#provider.on) {
|
||||
if (event === 'add') {
|
||||
this.provider.off(
|
||||
this.#provider.off(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
} else if (event === 'remove') {
|
||||
this.provider.off(
|
||||
this.#provider.off(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
} else if (event === 'reorder') {
|
||||
this.provider.off(
|
||||
this.#provider.off(
|
||||
this.domainObject,
|
||||
'reorder',
|
||||
this.onProviderReorder,
|
||||
this.#onProviderReorder,
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Add a domain object to this composition.
|
||||
*
|
||||
* A call to [load]{@link module:openmct.CompositionCollection#load}
|
||||
* must have resolved before using this method.
|
||||
*
|
||||
* @param {module:openmct.DomainObject} child the domain object to add
|
||||
* @param {boolean} skipMutate true if the underlying provider should
|
||||
* not be updated
|
||||
* @memberof module:openmct.CompositionCollection#
|
||||
* @name add
|
||||
* **TODO:** Remove `skipMutate` parameter.
|
||||
*
|
||||
* @param {DomainObject} child the domain object to add
|
||||
* @param {boolean} skipMutate
|
||||
* **Intended for internal use ONLY.**
|
||||
* true if the underlying provider should not be updated.
|
||||
*/
|
||||
CompositionCollection.prototype.add = function (child, skipMutate) {
|
||||
add(child, skipMutate) {
|
||||
if (!skipMutate) {
|
||||
if (!this.publicAPI.composition.checkPolicy(this.domainObject, child)) {
|
||||
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
|
||||
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
|
||||
}
|
||||
|
||||
this.provider.add(this.domainObject, child.identifier);
|
||||
this.#provider.add(this.domainObject, child.identifier);
|
||||
} else {
|
||||
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) {
|
||||
let keyString = this.publicAPI.objects.makeKeyString(child.identifier);
|
||||
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
|
||||
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
|
||||
|
||||
child = this.publicAPI.objects.toMutable(child);
|
||||
this.mutables[keyString] = child;
|
||||
child = this.#publicAPI.objects.toMutable(child);
|
||||
this.#mutables[keyString] = child;
|
||||
}
|
||||
|
||||
this.emit('add', child);
|
||||
this.#emit('add', child);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Load the domain objects in this composition.
|
||||
*
|
||||
* @returns {Promise.<Array.<module:openmct.DomainObject>>} a promise for
|
||||
* @param {AbortSignal} abortSignal
|
||||
* @returns {Promise.<Array.<DomainObject>>} a promise for
|
||||
* the domain objects in this composition
|
||||
* @memberof {module:openmct.CompositionCollection#}
|
||||
* @name load
|
||||
*/
|
||||
CompositionCollection.prototype.load = function (abortSignal) {
|
||||
this.cleanUpMutables();
|
||||
|
||||
return this.provider.load(this.domainObject)
|
||||
.then(function (children) {
|
||||
return Promise.all(children.map((c) => this.publicAPI.objects.get(c, abortSignal)));
|
||||
}.bind(this))
|
||||
.then(function (childObjects) {
|
||||
childObjects.forEach(c => this.add(c, true));
|
||||
|
||||
return childObjects;
|
||||
}.bind(this))
|
||||
.then(function (children) {
|
||||
this.emit('load');
|
||||
|
||||
return children;
|
||||
}.bind(this));
|
||||
};
|
||||
async load(abortSignal) {
|
||||
this.#cleanUpMutables();
|
||||
const children = await this.#provider.load(this.domainObject);
|
||||
const childObjects = await Promise.all(children.map((c) => this.#publicAPI.objects.get(c, abortSignal)));
|
||||
childObjects.forEach(c => this.add(c, true));
|
||||
this.#emit('load');
|
||||
|
||||
return childObjects;
|
||||
}
|
||||
/**
|
||||
* Remove a domain object from this composition.
|
||||
*
|
||||
* A call to [load]{@link module:openmct.CompositionCollection#load}
|
||||
* must have resolved before using this method.
|
||||
*
|
||||
* @param {module:openmct.DomainObject} child the domain object to remove
|
||||
* @param {boolean} skipMutate true if the underlying provider should
|
||||
* not be updated
|
||||
* @memberof module:openmct.CompositionCollection#
|
||||
* **TODO:** Remove `skipMutate` parameter.
|
||||
*
|
||||
* @param {DomainObject} child the domain object to remove
|
||||
* @param {boolean} skipMutate
|
||||
* **Intended for internal use ONLY.**
|
||||
* true if the underlying provider should not be updated.
|
||||
* @name remove
|
||||
*/
|
||||
CompositionCollection.prototype.remove = function (child, skipMutate) {
|
||||
remove(child, skipMutate) {
|
||||
if (!skipMutate) {
|
||||
this.provider.remove(this.domainObject, child.identifier);
|
||||
this.#provider.remove(this.domainObject, child.identifier);
|
||||
} else {
|
||||
if (this.returnMutables) {
|
||||
let keyString = this.publicAPI.objects.makeKeyString(child);
|
||||
if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) {
|
||||
this.publicAPI.objects.destroyMutable(this.mutables[keyString]);
|
||||
delete this.mutables[keyString];
|
||||
let keyString = this.#publicAPI.objects.makeKeyString(child);
|
||||
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
|
||||
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
|
||||
delete this.#mutables[keyString];
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('remove', child);
|
||||
this.#emit('remove', child);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Reorder the domain objects in this composition.
|
||||
*
|
||||
@@ -270,67 +282,75 @@ define([
|
||||
*
|
||||
* @param {number} oldIndex
|
||||
* @param {number} newIndex
|
||||
* @memberof module:openmct.CompositionCollection#
|
||||
* @name remove
|
||||
*/
|
||||
CompositionCollection.prototype.reorder = function (oldIndex, newIndex, skipMutate) {
|
||||
this.provider.reorder(this.domainObject, oldIndex, newIndex);
|
||||
};
|
||||
|
||||
reorder(oldIndex, newIndex, _skipMutate) {
|
||||
this.#provider.reorder(this.domainObject, oldIndex, newIndex);
|
||||
}
|
||||
/**
|
||||
* Handle reorder from provider.
|
||||
* @private
|
||||
* Destroy mutationListener
|
||||
*/
|
||||
CompositionCollection.prototype.onProviderReorder = function (reorderMap) {
|
||||
this.emit('reorder', reorderMap);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle adds from provider.
|
||||
* @private
|
||||
*/
|
||||
CompositionCollection.prototype.onProviderAdd = function (childId) {
|
||||
return this.publicAPI.objects.get(childId).then(function (child) {
|
||||
this.add(child, true);
|
||||
|
||||
return child;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle removal from provider.
|
||||
* @private
|
||||
*/
|
||||
CompositionCollection.prototype.onProviderRemove = function (child) {
|
||||
this.remove(child, true);
|
||||
};
|
||||
|
||||
CompositionCollection.prototype._destroy = function () {
|
||||
_destroy() {
|
||||
if (this.mutationListener) {
|
||||
this.mutationListener();
|
||||
delete this.mutationListener;
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Handle reorder from provider.
|
||||
* @private
|
||||
* @param {object} reorderMap
|
||||
*/
|
||||
#onProviderReorder(reorderMap) {
|
||||
this.#emit('reorder', reorderMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle adds from provider.
|
||||
* @private
|
||||
* @param {import('../objects/ObjectAPI').Identifier} childId
|
||||
* @returns {DomainObject}
|
||||
*/
|
||||
#onProviderAdd(childId) {
|
||||
return this.#publicAPI.objects.get(childId).then(function (child) {
|
||||
this.add(child, true);
|
||||
|
||||
return child;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle removal from provider.
|
||||
* @param {DomainObject} child
|
||||
*/
|
||||
#onProviderRemove(child) {
|
||||
this.remove(child, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit events.
|
||||
*
|
||||
* @private
|
||||
* @param {string} event
|
||||
* @param {...args.<any>} payload
|
||||
*/
|
||||
CompositionCollection.prototype.emit = function (event, ...payload) {
|
||||
this.listeners[event].forEach(function (l) {
|
||||
#emit(event, ...payload) {
|
||||
this.#listeners[event].forEach(function (l) {
|
||||
if (l.context) {
|
||||
l.callback.apply(l.context, payload);
|
||||
} else {
|
||||
l.callback(...payload);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
CompositionCollection.prototype.cleanUpMutables = function () {
|
||||
Object.values(this.mutables).forEach(mutable => {
|
||||
this.publicAPI.objects.destroyMutable(mutable);
|
||||
/**
|
||||
* Destroy all mutables.
|
||||
* @private
|
||||
*/
|
||||
#cleanUpMutables() {
|
||||
Object.values(this.#mutables).forEach(mutable => {
|
||||
this.#publicAPI.objects.destroyMutable(mutable);
|
||||
});
|
||||
};
|
||||
|
||||
return CompositionCollection;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
262
src/api/composition/CompositionProvider.js
Normal file
@@ -0,0 +1,262 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
import objectUtils from "../objects/object-utils";
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./CompositionAPI').default} CompositionAPI
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||
*/
|
||||
|
||||
/**
|
||||
* A CompositionProvider provides the underlying implementation of
|
||||
* composition-related behavior for certain types of domain object.
|
||||
*
|
||||
* By default, a composition provider will not support composition
|
||||
* modification. You can add support for mutation of composition by
|
||||
* defining `add` and/or `remove` methods.
|
||||
*
|
||||
* If the composition of an object can change over time-- perhaps via
|
||||
* server updates or mutation via the add/remove methods, then one must
|
||||
* trigger events as necessary.
|
||||
*
|
||||
*/
|
||||
export default class CompositionProvider {
|
||||
#publicAPI;
|
||||
#listeningTo;
|
||||
|
||||
/**
|
||||
* @param {OpenMCT} publicAPI
|
||||
* @param {CompositionAPI} compositionAPI
|
||||
*/
|
||||
constructor(publicAPI, compositionAPI) {
|
||||
this.#publicAPI = publicAPI;
|
||||
this.#listeningTo = {};
|
||||
|
||||
compositionAPI.addPolicy(this.#cannotContainItself.bind(this));
|
||||
compositionAPI.addPolicy(this.#supportsComposition.bind(this));
|
||||
}
|
||||
|
||||
get listeningTo() {
|
||||
return this.#listeningTo;
|
||||
}
|
||||
|
||||
get establishTopicListener() {
|
||||
return this.#establishTopicListener.bind(this);
|
||||
}
|
||||
|
||||
get publicAPI() {
|
||||
return this.#publicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this provider should be used to load composition for a
|
||||
* particular domain object.
|
||||
* @method appliesTo
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} domainObject the domain object
|
||||
* to check
|
||||
* @returns {boolean} true if this provider can provide composition for a given domain object
|
||||
*/
|
||||
appliesTo(domainObject) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
/**
|
||||
* Load any domain objects contained in the composition of this domain
|
||||
* object.
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* for which to load composition
|
||||
* @returns {Promise<Identifier[]>} a promise for
|
||||
* the Identifiers in this composition
|
||||
* @method load
|
||||
*/
|
||||
load(domainObject) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
/**
|
||||
* Attach listeners for changes to the composition of a given domain object.
|
||||
* Supports `add` and `remove` events.
|
||||
*
|
||||
* @param {DomainObject} domainObject to listen to
|
||||
* @param {string} event the event to bind to, either `add` or `remove`.
|
||||
* @param {Function} callback callback to invoke when event is triggered.
|
||||
* @param {any} [context] to use when invoking callback.
|
||||
*/
|
||||
on(domainObject,
|
||||
event,
|
||||
callback,
|
||||
context) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
/**
|
||||
* Remove a listener that was previously added for a given domain object.
|
||||
* event name, callback, and context must be the same as when the listener
|
||||
* was originally attached.
|
||||
*
|
||||
* @param {DomainObject} domainObject to remove listener for
|
||||
* @param {string} event event to stop listening to: `add` or `remove`.
|
||||
* @param {Function} callback callback to remove.
|
||||
* @param {any} context of callback to remove.
|
||||
*/
|
||||
off(domainObject,
|
||||
event,
|
||||
callback,
|
||||
context) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
/**
|
||||
* Remove a domain object from another domain object's composition.
|
||||
*
|
||||
* This method is optional; if not present, adding to a domain object's
|
||||
* composition using this provider will be disallowed.
|
||||
*
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* which should have its composition modified
|
||||
* @param {Identifier} childId the domain object to remove
|
||||
* @method remove
|
||||
*/
|
||||
remove(domainObject, childId) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
/**
|
||||
* Add a domain object to another domain object's composition.
|
||||
*
|
||||
* This method is optional; if not present, adding to a domain object's
|
||||
* composition using this provider will be disallowed.
|
||||
*
|
||||
* @param {DomainObject} parent the domain object
|
||||
* which should have its composition modified
|
||||
* @param {Identifier} childId the domain object to add
|
||||
* @method add
|
||||
*/
|
||||
add(parent, childId) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DomainObject} parent
|
||||
* @param {Identifier} childId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
includes(parent, childId) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DomainObject} domainObject
|
||||
* @param {number} oldIndex
|
||||
* @param {number} newIndex
|
||||
* @returns
|
||||
*/
|
||||
reorder(domainObject, oldIndex, newIndex) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens on general mutation topic, using injector to fetch to avoid
|
||||
* circular dependencies.
|
||||
* @private
|
||||
*/
|
||||
#establishTopicListener() {
|
||||
if (this.topicListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
|
||||
this.topicListener = () => {
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {DomainObject} parent
|
||||
* @param {DomainObject} child
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#cannotContainItself(parent, child) {
|
||||
return !(parent.identifier.namespace === child.identifier.namespace
|
||||
&& parent.identifier.key === child.identifier.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {DomainObject} parent
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#supportsComposition(parent, _child) {
|
||||
return this.#publicAPI.composition.supportsComposition(parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mutation events. If there are active listeners for the mutated
|
||||
* object, detects changes to composition and triggers necessary events.
|
||||
*
|
||||
* @private
|
||||
* @param {DomainObject} oldDomainObject
|
||||
*/
|
||||
#onMutation(oldDomainObject) {
|
||||
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
|
||||
const listeners = this.#listeningTo[id];
|
||||
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
|
||||
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
|
||||
|
||||
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
|
||||
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
|
||||
|
||||
function notify(value) {
|
||||
return function (listener) {
|
||||
if (listener.context) {
|
||||
listener.callback.call(listener.context, value);
|
||||
} else {
|
||||
listener.callback(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
listeners.composition = newComposition.map(objectUtils.parseKeyString);
|
||||
|
||||
added.forEach(function (addedChild) {
|
||||
listeners.add.forEach(notify(addedChild));
|
||||
});
|
||||
|
||||
removed.forEach(function (removedChild) {
|
||||
listeners.remove.forEach(notify(removedChild));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,102 +19,79 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import objectUtils from "../objects/object-utils";
|
||||
import CompositionProvider from './CompositionProvider';
|
||||
|
||||
define([
|
||||
'lodash',
|
||||
'objectUtils'
|
||||
], function (
|
||||
_,
|
||||
objectUtils
|
||||
) {
|
||||
/**
|
||||
* A CompositionProvider provides the underlying implementation of
|
||||
* composition-related behavior for certain types of domain object.
|
||||
*
|
||||
* By default, a composition provider will not support composition
|
||||
* modification. You can add support for mutation of composition by
|
||||
* defining `add` and/or `remove` methods.
|
||||
*
|
||||
* If the composition of an object can change over time-- perhaps via
|
||||
* server updates or mutation via the add/remove methods, then one must
|
||||
* trigger events as necessary.
|
||||
*
|
||||
* @interface CompositionProvider
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
|
||||
function DefaultCompositionProvider(publicAPI, compositionAPI) {
|
||||
this.publicAPI = publicAPI;
|
||||
this.listeningTo = {};
|
||||
this.onMutation = this.onMutation.bind(this);
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
|
||||
*/
|
||||
|
||||
this.cannotContainItself = this.cannotContainItself.bind(this);
|
||||
this.supportsComposition = this.supportsComposition.bind(this);
|
||||
/**
|
||||
* @typedef {import('./CompositionAPI').default} CompositionAPI
|
||||
*/
|
||||
|
||||
compositionAPI.addPolicy(this.cannotContainItself);
|
||||
compositionAPI.addPolicy(this.supportsComposition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.cannotContainItself = function (parent, child) {
|
||||
return !(parent.identifier.namespace === child.identifier.namespace
|
||||
&& parent.identifier.key === child.identifier.key);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.supportsComposition = function (parent, child) {
|
||||
return this.publicAPI.composition.supportsComposition(parent);
|
||||
};
|
||||
/**
|
||||
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||
*/
|
||||
|
||||
/**
|
||||
* A CompositionProvider provides the underlying implementation of
|
||||
* composition-related behavior for certain types of domain object.
|
||||
*
|
||||
* By default, a composition provider will not support composition
|
||||
* modification. You can add support for mutation of composition by
|
||||
* defining `add` and/or `remove` methods.
|
||||
*
|
||||
* If the composition of an object can change over time-- perhaps via
|
||||
* server updates or mutation via the add/remove methods, then one must
|
||||
* trigger events as necessary.
|
||||
* @extends CompositionProvider
|
||||
*/
|
||||
export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
/**
|
||||
* Check if this provider should be used to load composition for a
|
||||
* particular domain object.
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* @override
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* to check
|
||||
* @returns {boolean} true if this provider can provide
|
||||
* composition for a given domain object
|
||||
* @memberof module:openmct.CompositionProvider#
|
||||
* @method appliesTo
|
||||
* @returns {boolean} true if this provider can provide composition for a given domain object
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.appliesTo = function (domainObject) {
|
||||
appliesTo(domainObject) {
|
||||
return Boolean(domainObject.composition);
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Load any domain objects contained in the composition of this domain
|
||||
* object.
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* @override
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* for which to load composition
|
||||
* @returns {Promise.<Array.<module:openmct.Identifier>>} a promise for
|
||||
* @returns {Promise<Identifier[]>} a promise for
|
||||
* the Identifiers in this composition
|
||||
* @memberof module:openmct.CompositionProvider#
|
||||
* @method load
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.load = function (domainObject) {
|
||||
load(domainObject) {
|
||||
return Promise.all(domainObject.composition);
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Attach listeners for changes to the composition of a given domain object.
|
||||
* Supports `add` and `remove` events.
|
||||
*
|
||||
* @param {module:openmct.DomainObject} domainObject to listen to
|
||||
* @param String event the event to bind to, either `add` or `remove`.
|
||||
* @param Function callback callback to invoke when event is triggered.
|
||||
* @param [context] context to use when invoking callback.
|
||||
* @override
|
||||
* @param {DomainObject} domainObject to listen to
|
||||
* @param {string} event the event to bind to, either `add` or `remove`.
|
||||
* @param {Function} callback callback to invoke when event is triggered.
|
||||
* @param {any} [context] to use when invoking callback.
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.on = function (
|
||||
domainObject,
|
||||
on(domainObject,
|
||||
event,
|
||||
callback,
|
||||
context
|
||||
) {
|
||||
context) {
|
||||
this.establishTopicListener();
|
||||
|
||||
/** @type {string} */
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let objectListeners = this.listeningTo[keyString];
|
||||
|
||||
@@ -131,24 +108,24 @@ define([
|
||||
callback: callback,
|
||||
context: context
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Remove a listener that was previously added for a given domain object.
|
||||
* event name, callback, and context must be the same as when the listener
|
||||
* was originally attached.
|
||||
*
|
||||
* @param {module:openmct.DomainObject} domainObject to remove listener for
|
||||
* @param String event event to stop listening to: `add` or `remove`.
|
||||
* @param Function callback callback to remove.
|
||||
* @param [context] context of callback to remove.
|
||||
* @override
|
||||
* @param {DomainObject} domainObject to remove listener for
|
||||
* @param {string} event event to stop listening to: `add` or `remove`.
|
||||
* @param {Function} callback callback to remove.
|
||||
* @param {any} context of callback to remove.
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.off = function (
|
||||
domainObject,
|
||||
off(domainObject,
|
||||
event,
|
||||
callback,
|
||||
context
|
||||
) {
|
||||
context) {
|
||||
|
||||
/** @type {string} */
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
const objectListeners = this.listeningTo[keyString];
|
||||
|
||||
@@ -160,57 +137,64 @@ define([
|
||||
if (!objectListeners.add.length && !objectListeners.remove.length && !objectListeners.reorder.length) {
|
||||
delete this.listeningTo[keyString];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Remove a domain object from another domain object's composition.
|
||||
*
|
||||
* This method is optional; if not present, adding to a domain object's
|
||||
* composition using this provider will be disallowed.
|
||||
*
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* @override
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* which should have its composition modified
|
||||
* @param {module:openmct.DomainObject} child the domain object to remove
|
||||
* @memberof module:openmct.CompositionProvider#
|
||||
* @param {Identifier} childId the domain object to remove
|
||||
* @method remove
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.remove = function (domainObject, childId) {
|
||||
remove(domainObject, childId) {
|
||||
let composition = domainObject.composition.filter(function (child) {
|
||||
return !(childId.namespace === child.namespace
|
||||
&& childId.key === child.key);
|
||||
&& childId.key === child.key);
|
||||
});
|
||||
|
||||
this.publicAPI.objects.mutate(domainObject, 'composition', composition);
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Add a domain object to another domain object's composition.
|
||||
*
|
||||
* This method is optional; if not present, adding to a domain object's
|
||||
* composition using this provider will be disallowed.
|
||||
*
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* @override
|
||||
* @param {DomainObject} parent the domain object
|
||||
* which should have its composition modified
|
||||
* @param {module:openmct.DomainObject} child the domain object to add
|
||||
* @memberof module:openmct.CompositionProvider#
|
||||
* @param {Identifier} childId the domain object to add
|
||||
* @method add
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.add = function (parent, childId) {
|
||||
add(parent, childId) {
|
||||
if (!this.includes(parent, childId)) {
|
||||
parent.composition.push(childId);
|
||||
this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
* @param {DomainObject} parent
|
||||
* @param {Identifier} childId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.includes = function (parent, childId) {
|
||||
return parent.composition.some(composee =>
|
||||
this.publicAPI.objects.areIdsEqual(composee, childId));
|
||||
};
|
||||
includes(parent, childId) {
|
||||
return parent.composition.some(composee => this.publicAPI.objects.areIdsEqual(composee, childId));
|
||||
}
|
||||
|
||||
DefaultCompositionProvider.prototype.reorder = function (domainObject, oldIndex, newIndex) {
|
||||
/**
|
||||
* @override
|
||||
* @param {DomainObject} domainObject
|
||||
* @param {number} oldIndex
|
||||
* @param {number} newIndex
|
||||
* @returns
|
||||
*/
|
||||
reorder(domainObject, oldIndex, newIndex) {
|
||||
let newComposition = domainObject.composition.slice();
|
||||
let removeId = oldIndex > newIndex ? oldIndex + 1 : oldIndex;
|
||||
let insertPosition = oldIndex < newIndex ? newIndex + 1 : newIndex;
|
||||
@@ -241,6 +225,7 @@ define([
|
||||
|
||||
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
|
||||
|
||||
/** @type {string} */
|
||||
let id = objectUtils.makeKeyString(domainObject.identifier);
|
||||
const listeners = this.listeningTo[id];
|
||||
|
||||
@@ -257,66 +242,5 @@ define([
|
||||
listener.callback(reorderPlan);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Listens on general mutation topic, using injector to fetch to avoid
|
||||
* circular dependencies.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.establishTopicListener = function () {
|
||||
if (this.topicListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.publicAPI.objects.eventEmitter.on('mutation', this.onMutation);
|
||||
this.topicListener = () => {
|
||||
this.publicAPI.objects.eventEmitter.off('mutation', this.onMutation);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles mutation events. If there are active listeners for the mutated
|
||||
* object, detects changes to composition and triggers necessary events.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.onMutation = function (oldDomainObject) {
|
||||
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
|
||||
const listeners = this.listeningTo[id];
|
||||
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
|
||||
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
|
||||
|
||||
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
|
||||
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
|
||||
|
||||
function notify(value) {
|
||||
return function (listener) {
|
||||
if (listener.context) {
|
||||
listener.callback.call(listener.context, value);
|
||||
} else {
|
||||
listener.callback(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
listeners.composition = newComposition.map(objectUtils.parseKeyString);
|
||||
|
||||
added.forEach(function (addedChild) {
|
||||
listeners.add.forEach(notify(addedChild));
|
||||
});
|
||||
|
||||
removed.forEach(function (removedChild) {
|
||||
listeners.remove.forEach(notify(removedChild));
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
return DefaultCompositionProvider;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,11 @@
|
||||
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 extends EventEmitter {
|
||||
export default class FormsAPI {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.formController = new FormController(openmct);
|
||||
}
|
||||
@@ -92,29 +90,75 @@ export default class FormsAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
showForm(formStructure, {
|
||||
showCustomForm(formStructure, {
|
||||
element,
|
||||
onChange
|
||||
} = {}) {
|
||||
const changes = {};
|
||||
let overlay;
|
||||
let onDismiss;
|
||||
let onSave;
|
||||
if (element === undefined) {
|
||||
throw Error('Required element parameter not provided');
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
const changes = {};
|
||||
let formSave;
|
||||
let formCancel;
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
onSave = onFormAction(resolve);
|
||||
onDismiss = onFormAction(reject);
|
||||
formSave = onFormAction(resolve);
|
||||
formCancel = onFormAction(reject);
|
||||
});
|
||||
|
||||
const vm = new Vue({
|
||||
@@ -126,26 +170,17 @@ export default class FormsAPI extends EventEmitter {
|
||||
return {
|
||||
formStructure,
|
||||
onChange: onFormPropertyChange,
|
||||
onDismiss,
|
||||
onSave
|
||||
onCancel: formCancel,
|
||||
onSave: formSave
|
||||
};
|
||||
},
|
||||
template: '<FormProperties :model="formStructure" @onChange="onChange" @onDismiss="onDismiss" @onSave="onSave"></FormProperties>'
|
||||
template: '<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
|
||||
}).$mount();
|
||||
|
||||
const formElement = vm.$el;
|
||||
if (element) {
|
||||
element.append(formElement);
|
||||
} else {
|
||||
overlay = self.openmct.overlays.overlay({
|
||||
element: vm.$el,
|
||||
size: 'dialog',
|
||||
onDestroy: () => vm.$destroy()
|
||||
});
|
||||
}
|
||||
element.append(formElement);
|
||||
|
||||
function onFormPropertyChange(data) {
|
||||
self.emit('onFormPropertyChange', data);
|
||||
if (onChange) {
|
||||
onChange(data);
|
||||
}
|
||||
@@ -158,17 +193,14 @@ export default class FormsAPI extends EventEmitter {
|
||||
key = property.join('.');
|
||||
}
|
||||
|
||||
changes[key] = data.value;
|
||||
_.set(changes, key, data.value);
|
||||
}
|
||||
}
|
||||
|
||||
function onFormAction(callback) {
|
||||
return () => {
|
||||
if (element) {
|
||||
formElement.remove();
|
||||
} else {
|
||||
overlay.dismiss();
|
||||
}
|
||||
formElement.remove();
|
||||
vm.$destroy();
|
||||
|
||||
if (callback) {
|
||||
callback(changes);
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('The Forms API', () => {
|
||||
});
|
||||
|
||||
it('when container element is provided', (done) => {
|
||||
openmct.forms.showForm(formStructure, { element }).catch(() => {
|
||||
openmct.forms.showCustomForm(formStructure, { element }).catch(() => {
|
||||
done();
|
||||
});
|
||||
const titleElement = element.querySelector('.c-overlay__dialog-title');
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
tabindex="0"
|
||||
class="c-button js-cancel-button"
|
||||
aria-label="Cancel"
|
||||
@click="onDismiss"
|
||||
@click="onCancel"
|
||||
>
|
||||
{{ cancelLabel }}
|
||||
</button>
|
||||
@@ -164,8 +164,8 @@ export default {
|
||||
|
||||
this.$emit('onChange', data);
|
||||
},
|
||||
onDismiss() {
|
||||
this.$emit('onDismiss');
|
||||
onCancel() {
|
||||
this.$emit('onCancel');
|
||||
},
|
||||
onSave() {
|
||||
this.$emit('onSave');
|
||||
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
|
||||
this.formControl.show(this.$refs.rowElement, this.row, this.onChange);
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
const destroy = this.formControl.destroy;
|
||||
if (destroy) {
|
||||
destroy();
|
||||
|
||||
@@ -173,7 +173,7 @@ export default {
|
||||
this.options = this.model.options;
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
document.body.removeEventListener('click', this.handleOutsideClick);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
v-model="selected"
|
||||
required="model.required"
|
||||
name="mctControl"
|
||||
:aria-label="model.ariaLabel || model.name"
|
||||
@change="onChange($event)"
|
||||
>
|
||||
<option
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
:class="model.cssClass"
|
||||
>
|
||||
<textarea
|
||||
:id="`${model.key}-textarea`"
|
||||
v-model="field"
|
||||
type="text"
|
||||
:size="model.size"
|
||||
|
||||
@@ -3,39 +3,50 @@
|
||||
class="c-menu"
|
||||
:class="options.menuClass"
|
||||
>
|
||||
<ul v-if="options.actions.length && options.actions[0].length">
|
||||
<ul
|
||||
v-if="options.actions.length && options.actions[0].length"
|
||||
role="menu"
|
||||
>
|
||||
<template
|
||||
v-for="(actionGroups, index) in options.actions"
|
||||
:key="index"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
{{ 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>
|
||||
<div role="group">
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
<ul v-else>
|
||||
<ul
|
||||
v-else
|
||||
role="menu"
|
||||
>
|
||||
<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"
|
||||
|
||||
@@ -5,45 +5,52 @@
|
||||
>
|
||||
<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"
|
||||
:key="index"
|
||||
>
|
||||
<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()"
|
||||
>
|
||||
{{ 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>
|
||||
<div role="group">
|
||||
<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>
|
||||
</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"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* 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.
|
||||
@@ -45,7 +46,6 @@ export default class InterceptorRegistry {
|
||||
* @memberof module:openmct.InterceptorRegistry#
|
||||
*/
|
||||
addInterceptor(interceptorDef) {
|
||||
//TODO: sort by priority
|
||||
this.interceptors.push(interceptorDef);
|
||||
}
|
||||
|
||||
@@ -56,10 +56,18 @@ 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -75,11 +75,7 @@ class MutableDomainObject {
|
||||
return eventOff;
|
||||
}
|
||||
$set(path, value) {
|
||||
_.set(this, path, value);
|
||||
|
||||
if (path !== 'persisted' && path !== 'modified') {
|
||||
_.set(this, 'modified', Date.now());
|
||||
}
|
||||
MutableDomainObject.mutateObject(this, path, value);
|
||||
|
||||
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
|
||||
@@ -136,8 +132,11 @@ class MutableDomainObject {
|
||||
}
|
||||
|
||||
static mutateObject(object, path, value) {
|
||||
if (path !== 'persisted') {
|
||||
_.set(object, 'modified', Date.now());
|
||||
}
|
||||
|
||||
_.set(object, path, value);
|
||||
_.set(object, 'modified', Date.now());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
||||
/**
|
||||
* Uniquely identifies a domain object.
|
||||
*
|
||||
* @typedef Identifier
|
||||
* @typedef {object} Identifier
|
||||
* @property {string} namespace the namespace to/from which this domain
|
||||
* object should be loaded/stored.
|
||||
* @property {string} key a unique identifier for the domain object
|
||||
@@ -50,8 +50,8 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
||||
* A few common properties are defined for domain objects. Beyond these,
|
||||
* individual types of domain objects may add more as they see fit.
|
||||
*
|
||||
* @typedef DomainObject
|
||||
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
|
||||
* @typedef {object} DomainObject
|
||||
* @property {Identifier} identifier a key/namespace pair which
|
||||
* uniquely identifies this domain object
|
||||
* @property {string} type the type of domain object
|
||||
* @property {string} name the human-readable name for this domain object
|
||||
@@ -59,19 +59,19 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
||||
* object
|
||||
* @property {number} [modified] the time, in milliseconds since the UNIX
|
||||
* epoch, at which this domain object was last modified
|
||||
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
|
||||
* @property {Identifier[]} [composition] if
|
||||
* present, this will be used by the default composition provider
|
||||
* to load domain objects
|
||||
* @memberof module:openmct
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
*/
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @enum {String} SEARCH_TYPES
|
||||
* @property {String} OBJECTS Search for objects
|
||||
* @property {String} ANNOTATIONS Search for annotations
|
||||
* @property {String} TAGS Search for tags
|
||||
*/
|
||||
* @readonly
|
||||
* @enum {string} SEARCH_TYPES
|
||||
* @property {string} OBJECTS Search for objects
|
||||
* @property {string} ANNOTATIONS Search for annotations
|
||||
* @property {string} TAGS Search for tags
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utilities for loading, saving, and manipulating domain objects.
|
||||
@@ -96,7 +96,7 @@ export default class ObjectAPI {
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry();
|
||||
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation'];
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
|
||||
|
||||
this.errors = {
|
||||
Conflict: ConflictError
|
||||
@@ -204,13 +204,13 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
let dirtyObject;
|
||||
if (this.isTransactionActive()) {
|
||||
dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
}
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
if (this.isTransactionActive()) {
|
||||
let dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
}
|
||||
}
|
||||
|
||||
const provider = this.getProvider(identifier);
|
||||
@@ -354,39 +354,59 @@ export default class ObjectAPI {
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
save(domainObject) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
let savedResolve;
|
||||
let savedReject;
|
||||
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();
|
||||
if (domainObject.persisted === undefined) {
|
||||
result = new Promise((resolve, reject) => {
|
||||
savedResolve = resolve;
|
||||
savedReject = reject;
|
||||
});
|
||||
domainObject.persisted = persistedTime;
|
||||
const newObjectPromise = provider.create(domainObject);
|
||||
if (newObjectPromise) {
|
||||
newObjectPromise.then(response => {
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
savedResolve(response);
|
||||
}).catch((error) => {
|
||||
savedReject(error);
|
||||
});
|
||||
} else {
|
||||
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
|
||||
}
|
||||
const username = await this.#getCurrentUsername();
|
||||
const isNewObject = domainObject.persisted === undefined;
|
||||
let savedResolve;
|
||||
let savedReject;
|
||||
let savedObjectPromise;
|
||||
|
||||
result = new Promise((resolve, reject) => {
|
||||
savedResolve = resolve;
|
||||
savedReject = reject;
|
||||
});
|
||||
|
||||
this.#mutate(domainObject, 'modifiedBy', username);
|
||||
|
||||
if (isNewObject) {
|
||||
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 {
|
||||
domainObject.persisted = persistedTime;
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
result = provider.update(domainObject);
|
||||
lastPersistedTime = domainObject.persisted;
|
||||
const persistedTime = Date.now();
|
||||
this.#mutate(domainObject, 'persisted', persistedTime);
|
||||
|
||||
savedObjectPromise = provider.update(domainObject);
|
||||
}
|
||||
|
||||
if (savedObjectPromise) {
|
||||
savedObjectPromise.then(response => {
|
||||
savedResolve(response);
|
||||
}).catch((error) => {
|
||||
if (lastPersistedTime !== undefined) {
|
||||
this.#mutate(domainObject, 'persisted', lastPersistedTime);
|
||||
}
|
||||
|
||||
savedReject(error);
|
||||
});
|
||||
} else {
|
||||
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${savedObjectPromise} when ${isNewObject ? 'creating new' : 'updating'} object.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,8 +419,21 @@ export default class ObjectAPI {
|
||||
});
|
||||
}
|
||||
|
||||
async #getCurrentUsername() {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
let username;
|
||||
|
||||
if (user !== undefined) {
|
||||
username = user.getName();
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
|
||||
*
|
||||
* @returns {Transaction} a new Transaction that was just created
|
||||
*/
|
||||
startTransaction() {
|
||||
if (this.isTransactionActive()) {
|
||||
@@ -408,6 +441,8 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
this.transaction = new Transaction(this);
|
||||
|
||||
return this.transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -480,14 +515,16 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a domain object.
|
||||
* Modify a domain object. Internal to ObjectAPI, won't call save after.
|
||||
* @private
|
||||
*
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
mutate(domainObject, path, value) {
|
||||
#mutate(domainObject, path, value) {
|
||||
if (!this.supportsMutation(domainObject.identifier)) {
|
||||
throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
|
||||
}
|
||||
@@ -508,6 +545,18 @@ export default class ObjectAPI {
|
||||
//Destroy temporary mutable object
|
||||
this.destroyMutable(mutableDomainObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a domain object and save.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
mutate(domainObject, path, value) {
|
||||
this.#mutate(domainObject, path, value);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
this.transaction.add(domainObject);
|
||||
@@ -684,7 +733,7 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
isTransactionActive() {
|
||||
return Boolean(this.transaction && this.openmct.editor.isEditing());
|
||||
return this.transaction !== undefined && this.transaction !== null;
|
||||
}
|
||||
|
||||
#hasAlreadyBeenPersisted(domainObject) {
|
||||
|
||||
@@ -8,13 +8,27 @@ describe("The Object API", () => {
|
||||
let mockDomainObject;
|
||||
const TEST_NAMESPACE = "test-namespace";
|
||||
const TEST_KEY = "test-key";
|
||||
const USERNAME = 'Joan Q Public';
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
beforeEach((done) => {
|
||||
typeRegistry = jasmine.createSpyObj('typeRegistry', [
|
||||
'get'
|
||||
]);
|
||||
const userProvider = {
|
||||
isLoggedIn() {
|
||||
return true;
|
||||
},
|
||||
getCurrentUser() {
|
||||
return Promise.resolve({
|
||||
getName() {
|
||||
return USERNAME;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
openmct = createOpenMct();
|
||||
openmct.user.setProvider(userProvider);
|
||||
objectAPI = openmct.objects;
|
||||
|
||||
openmct.editor = {};
|
||||
@@ -63,19 +77,63 @@ describe("The Object API", () => {
|
||||
mockProvider.update.and.returnValue(Promise.resolve(true));
|
||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
});
|
||||
it("Calls 'create' on provider if object is new", () => {
|
||||
objectAPI.save(mockDomainObject);
|
||||
it("Adds a 'created' timestamp to new objects", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.created).not.toBeUndefined();
|
||||
});
|
||||
it("Calls 'create' on provider if object is new", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockProvider.create).toHaveBeenCalled();
|
||||
expect(mockProvider.update).not.toHaveBeenCalled();
|
||||
});
|
||||
it("Calls 'update' on provider if object is not new", () => {
|
||||
it("Calls 'update' on provider if object is not new", async () => {
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
objectAPI.save(mockDomainObject);
|
||||
await objectAPI.save(mockDomainObject);
|
||||
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);
|
||||
});
|
||||
it("Sets the current user for 'modifedBy' on existing objects", async () => {
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.modifiedBy).toBe(USERNAME);
|
||||
});
|
||||
|
||||
it("Does not persist if the object is unchanged", () => {
|
||||
mockDomainObject.persisted =
|
||||
|
||||
@@ -17,6 +17,7 @@ class Overlay extends EventEmitter {
|
||||
dismissable = true,
|
||||
element,
|
||||
onDestroy,
|
||||
onDismiss,
|
||||
size
|
||||
} = {}) {
|
||||
super();
|
||||
@@ -32,7 +33,7 @@ class Overlay extends EventEmitter {
|
||||
OverlayComponent: OverlayComponent
|
||||
},
|
||||
provide: {
|
||||
dismiss: this.dismiss.bind(this),
|
||||
dismiss: this.notifyAndDismiss.bind(this),
|
||||
element,
|
||||
buttons,
|
||||
dismissable: this.dismissable
|
||||
@@ -43,6 +44,10 @@ class Overlay extends EventEmitter {
|
||||
if (onDestroy) {
|
||||
this.once('destroy', onDestroy);
|
||||
}
|
||||
|
||||
if (onDismiss) {
|
||||
this.once('dismiss', onDismiss);
|
||||
}
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
@@ -51,6 +56,12 @@ 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
|
||||
**/
|
||||
|
||||
@@ -55,7 +55,7 @@ class OverlayAPI {
|
||||
dismissLastOverlay() {
|
||||
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
|
||||
if (lastOverlay && lastOverlay.dismissable) {
|
||||
lastOverlay.dismiss();
|
||||
lastOverlay.notifyAndDismiss();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import TelemetryMetadataManager from './TelemetryMetadataManager';
|
||||
import TelemetryValueFormatter from './TelemetryValueFormatter';
|
||||
import DefaultMetadataProvider from './DefaultMetadataProvider';
|
||||
import objectUtils from 'objectUtils';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class TelemetryAPI {
|
||||
|
||||
@@ -73,7 +72,7 @@ export default class TelemetryAPI {
|
||||
* @returns {boolean} true if the object is a telemetry object.
|
||||
*/
|
||||
isTelemetryObject(domainObject) {
|
||||
return Boolean(this.findMetadataProvider(domainObject));
|
||||
return Boolean(this.#findMetadataProvider(domainObject));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +86,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
canProvideTelemetry(domainObject) {
|
||||
return Boolean(this.findSubscriptionProvider(domainObject))
|
||||
return Boolean(this.#findSubscriptionProvider(domainObject))
|
||||
|| Boolean(this.findRequestProvider(domainObject));
|
||||
}
|
||||
|
||||
@@ -120,7 +119,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
findSubscriptionProvider() {
|
||||
#findSubscriptionProvider() {
|
||||
const args = Array.prototype.slice.apply(arguments);
|
||||
function supportsDomainObject(provider) {
|
||||
return provider.supportsSubscribe.apply(provider, args);
|
||||
@@ -130,9 +129,10 @@ export default class TelemetryAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Returns a telemetry request provider that supports
|
||||
* a given domain object and options.
|
||||
*/
|
||||
findRequestProvider(domainObject) {
|
||||
findRequestProvider() {
|
||||
const args = Array.prototype.slice.apply(arguments);
|
||||
function supportsDomainObject(provider) {
|
||||
return provider.supportsRequest.apply(provider, args);
|
||||
@@ -144,7 +144,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
findMetadataProvider(domainObject) {
|
||||
#findMetadataProvider(domainObject) {
|
||||
return this.metadataProviders.filter(function (p) {
|
||||
return p.supportsMetadata(domainObject);
|
||||
})[0];
|
||||
@@ -153,7 +153,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
findLimitEvaluator(domainObject) {
|
||||
#findLimitEvaluator(domainObject) {
|
||||
return this.limitProviders.filter(function (p) {
|
||||
return p.supportsLimits(domainObject);
|
||||
})[0];
|
||||
@@ -161,6 +161,7 @@ export default class TelemetryAPI {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Though used in TelemetryCollection as well
|
||||
*/
|
||||
standardizeRequestOptions(options) {
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
|
||||
@@ -174,6 +175,10 @@ export default class TelemetryAPI {
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
|
||||
options.domain = this.openmct.time.timeSystem().key;
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'timeContext')) {
|
||||
options.timeContext = this.openmct.time;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,7 +246,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* Request historical telemetry for a domain object.
|
||||
* The `options` argument allows you to specify filters
|
||||
* (start, end, etc.), sort order, and strategies for retrieving
|
||||
* (start, end, etc.), sort order, time context, and strategies for retrieving
|
||||
* telemetry (aggregation, latest available, etc.).
|
||||
*
|
||||
* @method request
|
||||
@@ -255,7 +260,7 @@ export default class TelemetryAPI {
|
||||
*/
|
||||
async request(domainObject) {
|
||||
if (this.noRequestProviderForAllObjects) {
|
||||
return Promise.resolve([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (arguments.length === 1) {
|
||||
@@ -273,22 +278,24 @@ export default class TelemetryAPI {
|
||||
if (!provider) {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
|
||||
return this.handleMissingRequestProvider(domainObject);
|
||||
return this.#handleMissingRequestProvider(domainObject);
|
||||
}
|
||||
|
||||
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
|
||||
try {
|
||||
const telemetry = await provider.request(...arguments);
|
||||
|
||||
return provider.request.apply(provider, arguments)
|
||||
.catch((rejected) => {
|
||||
if (rejected.name !== 'AbortError') {
|
||||
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
|
||||
console.error(rejected);
|
||||
}
|
||||
return telemetry;
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return Promise.reject(rejected);
|
||||
}).finally(() => {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
});
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,7 +313,7 @@ export default class TelemetryAPI {
|
||||
* the subscription
|
||||
*/
|
||||
subscribe(domainObject, callback, options) {
|
||||
const provider = this.findSubscriptionProvider(domainObject);
|
||||
const provider = this.#findSubscriptionProvider(domainObject);
|
||||
|
||||
if (!this.subscribeCache) {
|
||||
this.subscribeCache = {};
|
||||
@@ -353,7 +360,7 @@ export default class TelemetryAPI {
|
||||
*/
|
||||
getMetadata(domainObject) {
|
||||
if (!this.metadataCache.has(domainObject)) {
|
||||
const metadataProvider = this.findMetadataProvider(domainObject);
|
||||
const metadataProvider = this.#findMetadataProvider(domainObject);
|
||||
if (!metadataProvider) {
|
||||
return;
|
||||
}
|
||||
@@ -369,33 +376,6 @@ export default class TelemetryAPI {
|
||||
return this.metadataCache.get(domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of valueMetadatas that are common to all supplied
|
||||
* telemetry objects and match the requested hints.
|
||||
*
|
||||
*/
|
||||
commonValuesForHints(metadatas, hints) {
|
||||
const options = metadatas.map(function (metadata) {
|
||||
const values = metadata.valuesForHints(hints);
|
||||
|
||||
return _.keyBy(values, 'key');
|
||||
}).reduce(function (a, b) {
|
||||
const results = {};
|
||||
Object.keys(a).forEach(function (key) {
|
||||
if (Object.prototype.hasOwnProperty.call(b, key)) {
|
||||
results[key] = a[key];
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
const sortKeys = hints.map(function (h) {
|
||||
return 'hints.' + h;
|
||||
});
|
||||
|
||||
return _.sortBy(options, sortKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value formatter for a given valueMetadata.
|
||||
*
|
||||
@@ -450,7 +430,7 @@ export default class TelemetryAPI {
|
||||
*
|
||||
* @returns Promise
|
||||
*/
|
||||
handleMissingRequestProvider(domainObject) {
|
||||
#handleMissingRequestProvider(domainObject) {
|
||||
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
|
||||
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
|
||||
const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
|
||||
@@ -540,7 +520,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
getLimitEvaluator(domainObject) {
|
||||
const provider = this.findLimitEvaluator(domainObject);
|
||||
const provider = this.#findLimitEvaluator(domainObject);
|
||||
if (!provider) {
|
||||
return {
|
||||
evaluate: function () {}
|
||||
@@ -578,7 +558,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
getLimits(domainObject) {
|
||||
const provider = this.findLimitEvaluator(domainObject);
|
||||
const provider = this.#findLimitEvaluator(domainObject);
|
||||
if (!provider || !provider.getLimits) {
|
||||
return {
|
||||
limits: function () {
|
||||
|
||||
@@ -23,11 +23,11 @@ import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
import TelemetryAPI from './TelemetryAPI';
|
||||
import TelemetryCollection from './TelemetryCollection';
|
||||
|
||||
describe('Telemetry API', function () {
|
||||
describe('Telemetry API', () => {
|
||||
let openmct;
|
||||
let telemetryAPI;
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
openmct = {
|
||||
time: jasmine.createSpyObj('timeAPI', [
|
||||
'timeSystem',
|
||||
@@ -47,11 +47,11 @@ describe('Telemetry API', function () {
|
||||
|
||||
});
|
||||
|
||||
describe('telemetry providers', function () {
|
||||
describe('telemetry providers', () => {
|
||||
let telemetryProvider;
|
||||
let domainObject;
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
telemetryProvider = jasmine.createSpyObj('telemetryProvider', [
|
||||
'supportsSubscribe',
|
||||
'subscribe',
|
||||
@@ -73,19 +73,16 @@ describe('Telemetry API', function () {
|
||||
};
|
||||
});
|
||||
|
||||
it('provides consistent results without providers', function (done) {
|
||||
it('provides consistent results without providers', async () => {
|
||||
const unsubscribe = telemetryAPI.subscribe(domainObject);
|
||||
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
telemetryAPI.request(domainObject)
|
||||
.then((data) => {
|
||||
expect(data).toEqual([]);
|
||||
})
|
||||
.finally(done);
|
||||
const data = await telemetryAPI.request(domainObject);
|
||||
expect(data).toEqual([]);
|
||||
});
|
||||
|
||||
it('skips providers that do not match', function (done) {
|
||||
it('skips providers that do not match', async () => {
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(false);
|
||||
telemetryProvider.supportsRequest.and.returnValue(false);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
@@ -98,14 +95,13 @@ describe('Telemetry API', function () {
|
||||
expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
telemetryAPI.request(domainObject).then((response) => {
|
||||
expect(telemetryProvider.supportsRequest)
|
||||
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
|
||||
expect(telemetryProvider.request).not.toHaveBeenCalled();
|
||||
}).finally(done);
|
||||
await telemetryAPI.request(domainObject);
|
||||
expect(telemetryProvider.supportsRequest)
|
||||
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
|
||||
expect(telemetryProvider.request).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends subscribe calls to matching providers', function () {
|
||||
it('sends subscribe calls to matching providers', () => {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -133,7 +129,7 @@ describe('Telemetry API', function () {
|
||||
expect(callback).not.toHaveBeenCalledWith('otherValue');
|
||||
});
|
||||
|
||||
it('subscribes once per object', function () {
|
||||
it('subscribes once per object', () => {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -164,7 +160,7 @@ describe('Telemetry API', function () {
|
||||
expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue');
|
||||
});
|
||||
|
||||
it('only deletes subscription cache when there are no more subscribers', function () {
|
||||
it('only deletes subscription cache when there are no more subscribers', () => {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -187,7 +183,7 @@ describe('Telemetry API', function () {
|
||||
unsubscribeThree();
|
||||
});
|
||||
|
||||
it('does subscribe/unsubscribe', function () {
|
||||
it('does subscribe/unsubscribe', () => {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -203,7 +199,7 @@ describe('Telemetry API', function () {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('subscribes for different object', function () {
|
||||
it('subscribes for different object', () => {
|
||||
const unsubFuncs = [];
|
||||
const notifiers = [];
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -243,120 +239,120 @@ describe('Telemetry API', function () {
|
||||
expect(unsubFuncs[1]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends requests to matching providers', function (done) {
|
||||
it('sends requests to matching providers', async () => {
|
||||
const telemPromise = Promise.resolve([]);
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(telemPromise);
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request(domainObject).then(() => {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
}).finally(done);
|
||||
await telemetryAPI.request(domainObject);
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('generates default request options', function (done) {
|
||||
it('generates default request options', async () => {
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request(domainObject).then(() => {
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
await telemetryAPI.request(domainObject);
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
|
||||
telemetryProvider.supportsRequest.calls.reset();
|
||||
telemetryProvider.request.calls.reset();
|
||||
telemetryProvider.supportsRequest.calls.reset();
|
||||
telemetryProvider.request.calls.reset();
|
||||
|
||||
telemetryAPI.request(domainObject, {}).then(() => {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
});
|
||||
}).finally(done);
|
||||
await telemetryAPI.request(domainObject, {});
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('do not overwrite existing request options', function (done) {
|
||||
it('do not overwrite existing request options', async () => {
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request(domainObject, {
|
||||
await telemetryAPI.request(domainObject, {
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain'
|
||||
}).then(() => {
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal
|
||||
}
|
||||
);
|
||||
});
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal,
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal
|
||||
}
|
||||
);
|
||||
|
||||
}).finally(done);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal,
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('metadata', function () {
|
||||
describe('metadata', () => {
|
||||
let mockMetadata = {};
|
||||
let mockObjectType = {
|
||||
definition: {}
|
||||
};
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
telemetryAPI.addProvider({
|
||||
key: 'mockMetadataProvider',
|
||||
supportsMetadata() {
|
||||
@@ -369,7 +365,7 @@ describe('Telemetry API', function () {
|
||||
openmct.types.get.and.returnValue(mockObjectType);
|
||||
});
|
||||
|
||||
it('respects explicit priority', function () {
|
||||
it('respects explicit priority', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -408,7 +404,7 @@ describe('Telemetry API', function () {
|
||||
expect(value.hints.priority).toBe(index + 1);
|
||||
});
|
||||
});
|
||||
it('if no explicit priority, defaults to order defined', function () {
|
||||
it('if no explicit priority, defaults to order defined', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -435,7 +431,7 @@ describe('Telemetry API', function () {
|
||||
expect(value.key).toBe(mockMetadata.values[index].key);
|
||||
});
|
||||
});
|
||||
it('respects domain priority', function () {
|
||||
it('respects domain priority', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -477,7 +473,7 @@ describe('Telemetry API', function () {
|
||||
expect(values[0].key).toBe('timestamp-local');
|
||||
expect(values[1].key).toBe('timestamp-utc');
|
||||
});
|
||||
it('respects range priority', function () {
|
||||
it('respects range priority', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -519,7 +515,7 @@ describe('Telemetry API', function () {
|
||||
expect(values[0].key).toBe('cos');
|
||||
expect(values[1].key).toBe('sin');
|
||||
});
|
||||
it('respects priority and domain ordering', function () {
|
||||
it('respects priority and domain ordering', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "id",
|
||||
@@ -588,7 +584,7 @@ describe('Telemetry API', function () {
|
||||
definition: {}
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
openmct.telemetry = telemetryAPI;
|
||||
telemetryAPI.addProvider({
|
||||
key: 'mockMetadataProvider',
|
||||
@@ -644,16 +640,14 @@ describe('Telemetery', () => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('should not abort request without navigation', function (done) {
|
||||
it('should not abort request without navigation', async () => {
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request({}).finally(() => {
|
||||
expect(watchedSignal.aborted).toBe(false);
|
||||
done();
|
||||
});
|
||||
await telemetryAPI.request({});
|
||||
expect(watchedSignal.aborted).toBe(false);
|
||||
});
|
||||
|
||||
it('should abort request on navigation', function (done) {
|
||||
it('should abort request on navigation', (done) => {
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request({}).finally(() => {
|
||||
|
||||
@@ -202,8 +202,13 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
|
||||
getUpstreamContext() {
|
||||
let timeContext = this.globalTimeContext;
|
||||
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
|
||||
|
||||
@@ -229,6 +229,25 @@ describe("The Time API", function () {
|
||||
expect(api.clock()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Provides a default time context', () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
expect(timeContext).not.toBe(null);
|
||||
});
|
||||
|
||||
it("Without a clock, is in fixed time mode", () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
expect(timeContext.isRealTime()).toBe(false);
|
||||
});
|
||||
|
||||
it("Provided a clock, is in real-time mode", () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
timeContext.clock('mts', {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
expect(timeContext.isRealTime()).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
|
||||
|
||||
@@ -362,6 +362,18 @@ class TimeContext extends EventEmitter {
|
||||
this.boundsVal = newBounds;
|
||||
this.emit('bounds', this.boundsVal, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this time context is in real-time mode or not.
|
||||
* @returns {boolean} true if this context is in real-time mode, false if not
|
||||
*/
|
||||
isRealTime() {
|
||||
if (this.clock()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeContext;
|
||||
|
||||
@@ -114,6 +114,8 @@ export default {
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
|
||||
this.limitEvaluator = this.openmct
|
||||
.telemetry
|
||||
.limitEvaluator(this.domainObject);
|
||||
@@ -134,7 +136,8 @@ export default {
|
||||
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
strategy: 'latest',
|
||||
timeContext: this.timeContext
|
||||
});
|
||||
this.telemetryCollection.on('add', this.setLatestValues);
|
||||
this.telemetryCollection.on('clear', this.resetValues);
|
||||
@@ -144,7 +147,7 @@ export default {
|
||||
this.setUnit();
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
this.openmct.time.off('timeSystem', this.updateTimeSystem);
|
||||
this.telemetryCollection.off('add', this.setLatestValues);
|
||||
this.telemetryCollection.off('clear', this.resetValues);
|
||||
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
this.composition.on('reorder', this.reorder);
|
||||
this.composition.load();
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
this.composition.off('add', this.addItem);
|
||||
this.composition.off('remove', this.removeItem);
|
||||
this.composition.off('reorder', this.reorder);
|
||||
|
||||
@@ -33,11 +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"
|
||||
>
|
||||
<tr class="c-table__group-header js-lad-table-set__table-headers">
|
||||
<td colspan="10">
|
||||
{{ ladTable.domainObject.name }}
|
||||
</td>
|
||||
@@ -104,7 +102,7 @@ export default {
|
||||
this.composition.on('reorder', this.reorderLadTables);
|
||||
this.composition.load();
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
this.composition.off('add', this.addLadTable);
|
||||
this.composition.off('remove', this.removeLadTable);
|
||||
this.composition.off('reorder', this.reorderLadTables);
|
||||
|
||||
@@ -93,7 +93,7 @@ define([
|
||||
rowCount: 'reflow'
|
||||
},
|
||||
template: autoflowTemplate,
|
||||
destroyed: function () {
|
||||
unmounted: function () {
|
||||
controller.destroy();
|
||||
|
||||
if (interval) {
|
||||
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
});
|
||||
this.registerListeners();
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
if (this.plotResizeObserver) {
|
||||
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
||||
clearTimeout(this.resizeTimer);
|
||||
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
this.unobserveInterpolation = this.openmct.objects.observe(this.domainObject, 'configuration.useInterpolation', this.refreshData);
|
||||
this.unobserveBar = this.openmct.objects.observe(this.domainObject, 'configuration.useBar', this.refreshData);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
this.removeAllSubscriptions();
|
||||
|
||||
@@ -209,7 +209,7 @@ export default {
|
||||
this.registerListeners();
|
||||
this.composition.load();
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
this.stopListening();
|
||||
},
|
||||
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
this.initColorAndName();
|
||||
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
if (this.removeBarStylesListener) {
|
||||
this.removeBarStylesListener();
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.reloadTelemetry);
|
||||
this.unobserveUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.reloadTelemetry);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
if (!this.composition) {
|
||||
@@ -112,11 +112,7 @@ export default {
|
||||
}
|
||||
},
|
||||
removeFromComposition(telemetryObject) {
|
||||
let composition = this.domainObject.composition.filter(id =>
|
||||
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
|
||||
);
|
||||
|
||||
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
|
||||
this.composition.remove(telemetryObject);
|
||||
},
|
||||
addTelemetryObject(telemetryObject) {
|
||||
// grab information we need from the added telmetry object
|
||||
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
|
||||
this.$refs.plot.on('plotly_relayout', this.zoom);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
if (this.$refs.plot && this.$refs.plot.off) {
|
||||
this.$refs.plot.off('plotly_relayout', this.zoom);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
mounted() {
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -70,7 +70,7 @@ export default {
|
||||
this.registerListeners();
|
||||
this.composition.load();
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
@@ -104,10 +104,14 @@ export default {
|
||||
this.$set(this.plotSeries, this.plotSeries.length, series);
|
||||
this.setAxesLabels();
|
||||
},
|
||||
removeSeries(series) {
|
||||
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier));
|
||||
if (index !== undefined) {
|
||||
this.$delete(this.plotSeries, index);
|
||||
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);
|
||||
this.setAxesLabels();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -101,7 +101,7 @@ export default {
|
||||
this.registerListeners();
|
||||
this.composition.load();
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
mounted() {
|
||||
this.unlisten = ticker.listen(this.tick);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
mounted() {
|
||||
this.unlisten = ticker.listen(this.tick);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export default function ClockPlugin(options) {
|
||||
]
|
||||
},
|
||||
{
|
||||
ariaLabel: "12 or 24 hour clock",
|
||||
control: 'select',
|
||||
options: [
|
||||
{
|
||||
|
||||
@@ -295,7 +295,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
this.destroy();
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
this.composition.off('add', this.addTelemetryObject);
|
||||
this.composition.off('remove', this.removeTelemetryObject);
|
||||
if (this.conditionManager) {
|
||||
|
||||
@@ -180,7 +180,7 @@ export default {
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.resetApplied();
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -253,7 +253,7 @@ export default {
|
||||
return this.styleableFontItems.length && this.allowEditing;
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
this.removeListeners();
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
this.stylesManager.off('styleSelected', this.applyStyleToSelection);
|
||||
|
||||
@@ -75,7 +75,7 @@ export default {
|
||||
this.listenToConditionSetChanges();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.conditionSetIdentifier = null;
|
||||
|
||||
if (this.unlisten) {
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
padding: $interiorMarginLg $interiorMarginLg * 2;
|
||||
}
|
||||
|
||||
.c-condition-widget__label {
|
||||
padding: $interiorMargin;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
a.c-condition-widget {
|
||||
// Widget is conditionally made into a <a> when URL property has been defined
|
||||
cursor: pointer !important;
|
||||
|
||||
@@ -583,6 +583,7 @@ 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);
|
||||
|
||||
@@ -66,7 +66,7 @@ export default {
|
||||
this.openmct.selection.on('change', this.handleSelection);
|
||||
this.handleSelection(this.openmct.selection.get());
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
this.openmct.editor.off('isEditing', this.toggleEdit);
|
||||
this.openmct.selection.off('change', this.handleSelection);
|
||||
},
|
||||
|
||||
@@ -113,7 +113,7 @@ export default {
|
||||
this.removeSelectable = this.openmct.selection.selectable(
|
||||
this.$el, this.context, this.initSelect);
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ export default {
|
||||
|
||||
this.watchDisplayResize();
|
||||
},
|
||||
destroyed: function () {
|
||||
unmounted: function () {
|
||||
this.openmct.selection.off('change', this.setSelection);
|
||||
this.composition.off('add', this.addChild);
|
||||
this.composition.off('remove', this.removeChild);
|
||||
@@ -245,6 +245,9 @@ export default {
|
||||
});
|
||||
this.gridDimensions = [wMax * this.gridSize[0], hMax * this.gridSize[1]];
|
||||
},
|
||||
clearSelection() {
|
||||
this.$el.click();
|
||||
},
|
||||
watchDisplayResize() {
|
||||
const resizeObserver = new ResizeObserver(() => this.updateGrid());
|
||||
|
||||
@@ -478,7 +481,7 @@ export default {
|
||||
});
|
||||
_.pullAt(this.layoutItems, indices);
|
||||
this.mutate("configuration.items", this.layoutItems);
|
||||
this.$el.click();
|
||||
this.clearSelection();
|
||||
},
|
||||
untrackItem(item) {
|
||||
if (!item.identifier) {
|
||||
@@ -504,15 +507,11 @@ export default {
|
||||
}
|
||||
|
||||
if (!telemetryViewCount && !objectViewCount) {
|
||||
this.removeFromComposition(keyString);
|
||||
this.removeFromComposition(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);
|
||||
removeFromComposition(item) {
|
||||
this.composition.remove(item);
|
||||
},
|
||||
initializeItems() {
|
||||
this.telemetryViewMap = {};
|
||||
@@ -529,7 +528,10 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
this.startTransaction();
|
||||
removedItems.forEach(this.removeFromConfiguration);
|
||||
|
||||
return this.endTransaction();
|
||||
},
|
||||
isItemAlreadyTracked(child) {
|
||||
let found = false;
|
||||
@@ -590,7 +592,7 @@ export default {
|
||||
}
|
||||
});
|
||||
this.mutate("configuration.items", layoutItems);
|
||||
this.$el.click();
|
||||
this.clearSelection();
|
||||
},
|
||||
orderItem(position, selectedItems) {
|
||||
let delta = ORDERS[position];
|
||||
@@ -773,7 +775,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.$el.click(); //clear selection;
|
||||
this.clearSelection();
|
||||
|
||||
newDomainObjectsArray.forEach(domainObject => {
|
||||
this.composition.add(domainObject);
|
||||
@@ -867,6 +869,20 @@ 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;
|
||||
},
|
||||
|
||||
@@ -113,7 +113,7 @@ export default {
|
||||
this.removeSelectable = this.openmct.selection.selectable(
|
||||
this.$el, this.context, this.initSelect);
|
||||
},
|
||||
destroyed() {
|
||||
unmounted() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||