mirror of
https://github.com/nasa/openmct.git
synced 2025-01-11 23:42:41 +00:00
Merge branch 'master' into nb-embed-enhance
This commit is contained in:
commit
e48f419db7
@ -2,7 +2,7 @@ version: 2.1
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.25.2-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.29.0-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||
|
2
.github/workflows/e2e-couchdb.yml
vendored
2
.github/workflows/e2e-couchdb.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.25.2 install
|
||||
- run: npx playwright@1.29.0 install
|
||||
- run: npm install
|
||||
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||
- run: npm run test:e2e:couchdb
|
||||
|
2
.github/workflows/e2e-pr.yml
vendored
2
.github/workflows/e2e-pr.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.25.2 install
|
||||
- run: npx playwright@1.29.0 install
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: npm install
|
||||
- run: npm run test:e2e:full
|
||||
|
@ -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
|
||||
![Screen Shot 2022-11-23 at 9 51 36 AM](https://user-images.githubusercontent.com/4215777/203617422-4d912bfc-766f-4074-8324-409d9bbe7c05.png)
|
||||
|
||||
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).
|
||||
![Demo](https://nasa.github.io/openmct/static/res/images/Open-MCT.Browse.Layout.Mars-Weather-1.jpg)
|
||||
|
||||
## Building and Running Open MCT Locally
|
||||
|
||||
|
@ -8,7 +8,7 @@ This document is designed to capture on the What, Why, and How's of writing and
|
||||
|
||||
1. [Getting Started](#getting-started)
|
||||
2. [Types of Testing](#types-of-e2e-testing)
|
||||
3. [Architecture](#architecture)
|
||||
3. [Architecture](#test-architecture-and-ci)
|
||||
|
||||
## Getting Started
|
||||
|
||||
@ -276,14 +276,36 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
|
||||
- Leverage `await page.goto('./', { waitUntil: 'networkidle' });`
|
||||
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
|
||||
### How to write a great test (TODO)
|
||||
### How to write a great test (WIP)
|
||||
|
||||
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
|
||||
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
|
||||
|
||||
```js
|
||||
// Fill the "Notes" section with information about the
|
||||
// currently running test and its project.
|
||||
const { testNotes } = page;
|
||||
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
|
||||
await notesInput.fill(testNotes);
|
||||
```
|
||||
|
||||
#### How to write a great visual test (TODO)
|
||||
|
||||
#### How to write a great network test
|
||||
|
||||
- Where possible, it is best to mock out third-party network activity to ensure we are testing application behavior of Open MCT.
|
||||
- It is best to be as specific as possible about the expected network request/response structures in creating your mocks.
|
||||
- Make sure to only mock requests which are relevant to the specific behavior being tested.
|
||||
- Where possible, network requests and responses should be treated in an order-agnostic manner, as the order in which certain requests/responses happen is dynamic and subject to change.
|
||||
|
||||
Some examples of mocking network responses in regards to CouchDB can be found in our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) test file.
|
||||
|
||||
### Best Practices
|
||||
|
||||
For now, our best practices exist as self-tested, living documentation in our [exampleTemplate.e2e.spec.js](./tests/framework/exampleTemplate.e2e.spec.js) file.
|
||||
|
||||
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
|
||||
|
||||
### Tips & Tricks (TODO)
|
||||
|
||||
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
|
||||
@ -378,3 +400,23 @@ A single e2e test in Open MCT is extended to run:
|
||||
- Tests won't start because 'Error: <http://localhost:8080/># is already used...'
|
||||
This error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal:
|
||||
```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```
|
||||
|
||||
### Upgrading Playwright
|
||||
|
||||
In order to upgrade from one version of Playwright to another, the version should be updated in several places in both `openmct` and `openmct-yamcs` repos. An easy way to identify these locations is to search for the current version in all files and find/replace.
|
||||
|
||||
For reference, all of the locations where the version should be updated are listed below:
|
||||
|
||||
#### **In `openmct`:**
|
||||
|
||||
- `package.json`
|
||||
- Both packages `@playwright/test` and `playwright-core` should be updated to the same target version.
|
||||
- `.circleci/config.yml`
|
||||
- `.github/workflows/e2e-couchdb.yml`
|
||||
- `.github/workflows/e2e-pr.yml`
|
||||
|
||||
#### **In `openmct-yamcs`:**
|
||||
|
||||
- `package.json`
|
||||
- `@playwright/test` should be updated to the target version.
|
||||
- `.github/workflows/yamcs-quickstart-e2e.yml`
|
||||
|
@ -72,17 +72,19 @@ 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'
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
await nameInput.fill("");
|
||||
await nameInput.fill(name);
|
||||
|
||||
// Fill the "Notes" section with information about the
|
||||
// currently running test and its project.
|
||||
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
|
||||
await notesInput.fill(page.testNotes);
|
||||
if (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
|
||||
await Promise.all([
|
||||
|
@ -27,7 +27,7 @@
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
|
||||
test.describe("CouchDB Status Indicator @couchdb", () => {
|
||||
test.describe("CouchDB Status Indicator with mocked responses @couchdb", () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
//TODO BeforeAll Verify CouchDB Connectivity with APIContext
|
||||
test('Shows green if connected', async ({ page }) => {
|
||||
@ -71,38 +71,41 @@ test.describe("CouchDB Status Indicator @couchdb", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("CouchDB initialization @couchdb", () => {
|
||||
test.describe("CouchDB initialization with mocked responses @couchdb", () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
|
||||
// Store any relevant PUT requests that happen on the page
|
||||
const createMineFolderRequests = [];
|
||||
page.on('request', req => {
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (req.method() === 'PUT' && req.url().endsWith('openmct/mine')) {
|
||||
createMineFolderRequests.push(req);
|
||||
}
|
||||
});
|
||||
const mockedMissingObjectResponsefromCouchDB = {
|
||||
status: 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
};
|
||||
|
||||
// Override the first request to GET openmct/mine to return a 404
|
||||
await page.route('**/openmct/mine', route => {
|
||||
route.fulfill({
|
||||
status: 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
// Override the first request to GET openmct/mine to return a 404.
|
||||
// This simulates the case of starting Open MCT with a fresh database
|
||||
// and no "My Items" folder created yet.
|
||||
await page.route('**/mine', route => {
|
||||
route.fulfill(mockedMissingObjectResponsefromCouchDB);
|
||||
}, { times: 1 });
|
||||
|
||||
// Go to baseURL
|
||||
// Set up promise to verify that a PUT request to create "My Items"
|
||||
// folder was made.
|
||||
const putMineFolderRequest = page.waitForRequest(req =>
|
||||
req.url().endsWith('/mine')
|
||||
&& req.method() === 'PUT');
|
||||
|
||||
// Set up promise to verify that a GET request to retrieve "My Items"
|
||||
// folder was made.
|
||||
const getMineFolderRequest = page.waitForRequest(req =>
|
||||
req.url().endsWith('/mine')
|
||||
&& req.method() === 'GET');
|
||||
|
||||
// Go to baseURL.
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Verify that error banner is displayed
|
||||
const bannerMessage = await page.locator('.c-message-banner__message').innerText();
|
||||
expect(bannerMessage).toEqual('Failed to retrieve object mine');
|
||||
|
||||
// Verify that a PUT request to create "My Items" folder was made
|
||||
await expect.poll(() => createMineFolderRequests.length, {
|
||||
message: 'Verify that PUT request to create "mine" folder was made',
|
||||
timeout: 1000
|
||||
}).toBeGreaterThanOrEqual(1);
|
||||
// Wait for both requests to resolve.
|
||||
await Promise.all([
|
||||
putMineFolderRequest,
|
||||
getMineFolderRequest
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -24,8 +24,9 @@
|
||||
This test suite is dedicated to tests which verify form functionality in isolation
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const genUuid = require('uuid').v4;
|
||||
const path = require('path');
|
||||
|
||||
const TEST_FOLDER = 'test folder';
|
||||
@ -128,6 +129,108 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
timeout: 1000
|
||||
}).toEqual(1);
|
||||
});
|
||||
test('Can create an object after a conflict error @couchdb @2p', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5982'
|
||||
});
|
||||
|
||||
const page2 = await page.context().newPage();
|
||||
|
||||
// Both pages: Go to baseURL
|
||||
await Promise.all([
|
||||
page.goto('./', { waitUntil: 'networkidle' }),
|
||||
page2.goto('./', { waitUntil: 'networkidle' })
|
||||
]);
|
||||
|
||||
// Both pages: Click the Create button
|
||||
await Promise.all([
|
||||
page.click('button:has-text("Create")'),
|
||||
page2.click('button:has-text("Create")')
|
||||
]);
|
||||
|
||||
// Both pages: Click "Clock" in the Create menu
|
||||
await Promise.all([
|
||||
page.click(`li[role='menuitem']:text("Clock")`),
|
||||
page2.click(`li[role='menuitem']:text("Clock")`)
|
||||
]);
|
||||
|
||||
// Generate unique names for both objects
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
const nameInput2 = page2.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
|
||||
// Both pages: Fill in the 'Name' form field.
|
||||
await Promise.all([
|
||||
nameInput.fill(""),
|
||||
nameInput.fill(`Clock:${genUuid()}`),
|
||||
nameInput2.fill(""),
|
||||
nameInput2.fill(`Clock:${genUuid()}`)
|
||||
]);
|
||||
|
||||
// Both pages: Fill the "Notes" section with information about the
|
||||
// currently running test and its project.
|
||||
const testNotes = page.testNotes;
|
||||
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
|
||||
const notesInput2 = page2.locator('form[name="mctForm"] #notes-textarea');
|
||||
await Promise.all([
|
||||
notesInput.fill(testNotes),
|
||||
notesInput2.fill(testNotes)
|
||||
]);
|
||||
|
||||
// Page 2: Click "OK" to create the domain object and wait for navigation.
|
||||
// This will update the composition of the parent folder, setting the
|
||||
// conditions for a conflict error from the first page.
|
||||
await Promise.all([
|
||||
page2.waitForLoadState(),
|
||||
page2.click('[aria-label="Save"]'),
|
||||
// Wait for Save Banner to appear
|
||||
page2.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// Close Page 2, we're done with it.
|
||||
await page2.close();
|
||||
|
||||
// Page 1: Click "OK" to create the domain object and wait for navigation.
|
||||
// This will trigger a conflict error upon attempting to update
|
||||
// the composition of the parent folder.
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
page.click('[aria-label="Save"]'),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// Page 1: Verify that the conflict has occurred and an error notification is displayed.
|
||||
await expect(page.locator('.c-message-banner__message', {
|
||||
hasText: "Conflict detected while saving mine"
|
||||
})).toBeVisible();
|
||||
|
||||
// Page 1: Start logging console errors from this point on
|
||||
let errors = [];
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
// Page 1: Try to create a clock with the page that received the conflict.
|
||||
const clockAfterConflict = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock'
|
||||
});
|
||||
|
||||
// Page 1: Wait for save progress dialog to appear/disappear
|
||||
await page.locator('.c-message-banner__message', {
|
||||
hasText: 'Do not navigate away from this page or close this browser tab while this message is displayed.',
|
||||
state: 'visible'
|
||||
}).waitFor({ state: 'hidden' });
|
||||
|
||||
// Page 1: Navigate to 'My Items' and verify that the second clock was created
|
||||
await page.goto('./#/browse/mine');
|
||||
await expect(page.locator(`.c-grid-item__name[title="${clockAfterConflict.name}"]`)).toBeVisible();
|
||||
|
||||
// Verify no console errors occurred
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Form Correctness by Object Type', () => {
|
||||
|
@ -24,22 +24,19 @@
|
||||
* This test suite is dedicated to testing the Gauge component.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const uuid = require('uuid').v4;
|
||||
|
||||
test.describe('Gauge', () => {
|
||||
let 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' });
|
||||
|
||||
// Create the gauge
|
||||
gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' });
|
||||
});
|
||||
|
||||
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"]');
|
||||
|
||||
@ -90,4 +87,38 @@ test.describe('Gauge', () => {
|
||||
// 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
|
||||
});
|
||||
});
|
||||
|
@ -44,6 +44,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
||||
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`Entry ${iteration}`);
|
||||
await page.locator(entryLocator).press('Enter');
|
||||
}
|
||||
|
||||
return notebook;
|
||||
|
@ -26,7 +26,7 @@
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const { createDomainObjectWithDefaults} = require('../../../../appActions');
|
||||
|
||||
test.describe('Plot Integrity Testing @unstable', () => {
|
||||
let sineWaveGeneratorObject;
|
||||
@ -40,7 +40,6 @@ test.describe('Plot Integrity Testing @unstable', () => {
|
||||
test('Plots do not re-request data when a plot is clicked', async ({ page }) => {
|
||||
//Navigate to Sine Wave Generator
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
//Capture the number of plots points and store as const name numberOfPlotPoints
|
||||
//Click on the plot canvas
|
||||
await page.locator('canvas').nth(1).click();
|
||||
//No request was made to get historical data
|
||||
@ -51,4 +50,90 @@ test.describe('Plot Integrity Testing @unstable', () => {
|
||||
});
|
||||
expect(createMineFolderRequests.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('Plot is rendered when infinity values exist', async ({ page }) => {
|
||||
// Edit Plot
|
||||
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
|
||||
|
||||
//Get pixel data from Canvas
|
||||
const plotPixelSize = await getCanvasPixelsWithData(page);
|
||||
expect(plotPixelSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This function edits a sine wave generator with the default options and enables the infinity values option.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('../../../../appActions').CreateObjectInfo} sineWaveGeneratorObject
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the edited domain object.
|
||||
*/
|
||||
async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) {
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
// Edit LAD table
|
||||
await page.locator('[title="More options"]').click();
|
||||
await page.locator('[title="Edit properties of this object."]').click();
|
||||
// Modify the infinity option to true
|
||||
const infinityInput = page.locator('[aria-label="Include Infinity Values"]');
|
||||
await infinityInput.click();
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
page.click('[aria-label="Save"]'),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// FIXME: Changes to SWG properties should be reflected on save, but they're not?
|
||||
// Thus, navigate away and back to the object.
|
||||
await page.goto('./#/browse/mine');
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
|
||||
await page.locator('c-progress-bar c-telemetry-table__progress-bar').waitFor({
|
||||
state: 'hidden'
|
||||
});
|
||||
|
||||
// FIXME: The progress bar disappears on series data load, not on plot render,
|
||||
// so wait for a half a second before evaluating the canvas.
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getCanvasPixelsWithData(page) {
|
||||
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
|
||||
|
||||
await page.evaluate(() => {
|
||||
// The document canvas is where the plot points and lines are drawn.
|
||||
// The only way to access the canvas is using document (using page.evaluate)
|
||||
let data;
|
||||
let canvas;
|
||||
let ctx;
|
||||
canvas = document.querySelector('canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const imageDataValues = Object.values(data);
|
||||
let plotPixels = [];
|
||||
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
|
||||
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
|
||||
for (let i = 0; i < imageDataValues.length;) {
|
||||
if (imageDataValues[i] > 0) {
|
||||
plotPixels.push({
|
||||
startIndex: i,
|
||||
endIndex: i + 3,
|
||||
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
|
||||
});
|
||||
}
|
||||
|
||||
i = i + 4;
|
||||
|
||||
}
|
||||
|
||||
window.getCanvasValue(plotPixels.length);
|
||||
});
|
||||
|
||||
return getTelemValuePromise;
|
||||
}
|
||||
|
@ -33,7 +33,8 @@ define([
|
||||
dataRateInHz: 1,
|
||||
randomness: 0,
|
||||
phase: 0,
|
||||
loadDelay: 0
|
||||
loadDelay: 0,
|
||||
infinityValues: false
|
||||
};
|
||||
|
||||
function GeneratorProvider(openmct) {
|
||||
@ -56,7 +57,8 @@ define([
|
||||
'dataRateInHz',
|
||||
'randomness',
|
||||
'phase',
|
||||
'loadDelay'
|
||||
'loadDelay',
|
||||
'infinityValues'
|
||||
];
|
||||
|
||||
request = request || {};
|
||||
|
@ -76,10 +76,10 @@
|
||||
name: data.name,
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
|
||||
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues),
|
||||
wavelengths: wavelengths(),
|
||||
intensities: intensities(),
|
||||
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
|
||||
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues)
|
||||
}
|
||||
});
|
||||
nextStep += step;
|
||||
@ -117,6 +117,7 @@
|
||||
var phase = request.phase;
|
||||
var randomness = request.randomness;
|
||||
var loadDelay = Math.max(request.loadDelay, 0);
|
||||
var infinityValues = request.infinityValues;
|
||||
|
||||
var step = 1000 / dataRateInHz;
|
||||
var nextStep = start - (start % step) + step;
|
||||
@ -127,10 +128,10 @@
|
||||
data.push({
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
|
||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues),
|
||||
wavelengths: wavelengths(),
|
||||
intensities: intensities(),
|
||||
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
|
||||
cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues)
|
||||
});
|
||||
}
|
||||
|
||||
@ -155,12 +156,20 @@
|
||||
});
|
||||
}
|
||||
|
||||
function cos(timestamp, period, amplitude, offset, phase, randomness) {
|
||||
function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
|
||||
if (infinityValues && Math.random() > 0.5) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
return amplitude
|
||||
* Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
||||
}
|
||||
|
||||
function sin(timestamp, period, amplitude, offset, phase, randomness) {
|
||||
function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
|
||||
if (infinityValues && Math.random() > 0.5) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
return amplitude
|
||||
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
||||
}
|
||||
|
@ -143,6 +143,16 @@ define([
|
||||
"telemetry",
|
||||
"loadDelay"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Include Infinity Values",
|
||||
control: "toggleSwitch",
|
||||
cssClass: "l-input",
|
||||
key: "infinityValues",
|
||||
property: [
|
||||
"telemetry",
|
||||
"infinityValues"
|
||||
]
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
@ -153,7 +163,8 @@ define([
|
||||
dataRateInHz: 1,
|
||||
phase: 0,
|
||||
randomness: 0,
|
||||
loadDelay: 0
|
||||
loadDelay: 0,
|
||||
infinityValues: false
|
||||
};
|
||||
}
|
||||
});
|
||||
|
34
package.json
34
package.json
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.3-SNAPSHOT",
|
||||
"version": "2.1.6-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.13.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.3.0",
|
||||
"@types/lodash": "4.14.188",
|
||||
"babel-loader": "9.0.0",
|
||||
"@playwright/test": "1.29.0",
|
||||
"@types/jasmine": "4.3.1",
|
||||
"@types/lodash": "4.14.191",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"codecov": "3.8.3",
|
||||
"comma-separated-values": "3.6.4",
|
||||
@ -19,10 +19,10 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.27.0",
|
||||
"eslint": "8.30.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.11.2",
|
||||
"eslint-plugin-vue": "9.7.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",
|
||||
@ -38,33 +38,33 @@
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"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.38",
|
||||
"moment-timezone": "0.5.40",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"playwright-core": "1.25.2",
|
||||
"plotly.js-basic-dist": "2.14.0",
|
||||
"playwright-core": "1.29.0",
|
||||
"plotly.js-basic-dist": "2.17.0",
|
||||
"plotly.js-gl2d-dist": "2.14.0",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.56.1",
|
||||
"sass-loader": "13.0.2",
|
||||
"sinon": "14.0.1",
|
||||
"sinon": "15.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "4.8.4",
|
||||
"typescript": "4.9.4",
|
||||
"uuid": "9.0.0",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "9.1.0",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"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"
|
||||
},
|
||||
|
@ -73,6 +73,10 @@ export default class Editor extends EventEmitter {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.openmct.objects.getActiveTransaction();
|
||||
if (!transaction) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
transaction.cancel()
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
|
@ -29,6 +29,7 @@
|
||||
<ToggleSwitch
|
||||
id="switchId"
|
||||
:checked="isChecked"
|
||||
:name="model.name"
|
||||
@change="toggleCheckBox"
|
||||
/>
|
||||
</span>
|
||||
|
@ -193,23 +193,27 @@ export default class ObjectAPI {
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {string} key the key for the domain object to load
|
||||
* @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
|
||||
* @param {boolean} forceRemote defaults to false. If true, will skip cached and
|
||||
* dirty/in-transaction objects use and the provider.get method
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
get(identifier, abortSignal) {
|
||||
get(identifier, abortSignal, forceRemote = false) {
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
if (!forceRemote) {
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
let dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
if (this.isTransactionActive()) {
|
||||
let dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,6 +361,7 @@ export default class ObjectAPI {
|
||||
async save(domainObject) {
|
||||
const provider = this.getProvider(domainObject.identifier);
|
||||
let result;
|
||||
let lastPersistedTime;
|
||||
|
||||
if (!this.isPersistable(domainObject.identifier)) {
|
||||
result = Promise.reject('Object provider does not support saving');
|
||||
@ -387,9 +392,9 @@ export default class ObjectAPI {
|
||||
|
||||
savedObjectPromise = provider.create(domainObject);
|
||||
} else {
|
||||
lastPersistedTime = domainObject.persisted;
|
||||
const persistedTime = Date.now();
|
||||
this.#mutate(domainObject, 'persisted', persistedTime);
|
||||
|
||||
savedObjectPromise = provider.update(domainObject);
|
||||
}
|
||||
|
||||
@ -397,6 +402,10 @@ export default class ObjectAPI {
|
||||
savedObjectPromise.then(response => {
|
||||
savedResolve(response);
|
||||
}).catch((error) => {
|
||||
if (!isNewObject) {
|
||||
this.#mutate(domainObject, 'persisted', lastPersistedTime);
|
||||
}
|
||||
|
||||
savedReject(error);
|
||||
});
|
||||
} else {
|
||||
@ -404,9 +413,20 @@ export default class ObjectAPI {
|
||||
}
|
||||
}
|
||||
|
||||
return result.catch((error) => {
|
||||
return result.catch(async (error) => {
|
||||
if (error instanceof this.errors.Conflict) {
|
||||
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
|
||||
// Synchronized objects will resolve their own conflicts
|
||||
if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
|
||||
this.openmct.notifications.info(`Conflict detected while saving "${this.makeKeyString(domainObject.name)}", attempting to resolve`);
|
||||
} else {
|
||||
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
this.endTransaction();
|
||||
}
|
||||
|
||||
await this.refresh(domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
@ -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
|
||||
|
@ -53,10 +53,7 @@ export default class CreateAction extends PropertiesAction {
|
||||
|
||||
const existingValue = this.domainObject[key];
|
||||
if (!(existingValue instanceof Array) && (typeof existingValue === 'object')) {
|
||||
value = {
|
||||
...existingValue,
|
||||
...value
|
||||
};
|
||||
value = _.merge(existingValue, value);
|
||||
}
|
||||
|
||||
_.set(this.domainObject, key, value);
|
||||
@ -76,19 +73,21 @@ export default class CreateAction extends PropertiesAction {
|
||||
title: 'Saving'
|
||||
});
|
||||
|
||||
const success = await this.openmct.objects.save(this.domainObject);
|
||||
if (success) {
|
||||
try {
|
||||
await this.openmct.objects.save(this.domainObject);
|
||||
const compositionCollection = await this.openmct.composition.get(parentDomainObject);
|
||||
compositionCollection.add(this.domainObject);
|
||||
|
||||
this._navigateAndEdit(this.domainObject, parentDomainObjectPath);
|
||||
|
||||
this.openmct.notifications.info('Save successful');
|
||||
} else {
|
||||
this.openmct.notifications.error('Error saving objects');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.openmct.notifications.error(`Error saving objects: ${err}`);
|
||||
} finally {
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
import PropertiesAction from './PropertiesAction';
|
||||
import CreateWizard from './CreateWizard';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class EditPropertiesAction extends PropertiesAction {
|
||||
constructor(openmct) {
|
||||
@ -61,10 +62,7 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
Object.entries(changes).forEach(([key, value]) => {
|
||||
const existingValue = this.domainObject[key];
|
||||
if (!(Array.isArray(existingValue)) && (typeof existingValue === 'object')) {
|
||||
value = {
|
||||
...existingValue,
|
||||
...value
|
||||
};
|
||||
value = _.merge(existingValue, value);
|
||||
}
|
||||
|
||||
this.openmct.objects.mutate(this.domainObject, key, value);
|
||||
|
@ -45,6 +45,10 @@ export default class GoToOriginalAction {
|
||||
});
|
||||
}
|
||||
appliesTo(objectPath) {
|
||||
if (this._openmct.editor.isEditing()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let parentKeystring = objectPath[1] && this._openmct.objects.makeKeyString(objectPath[1].identifier);
|
||||
|
||||
if (!parentKeystring) {
|
||||
|
@ -31,21 +31,32 @@
|
||||
:title="image.formattedTime"
|
||||
>
|
||||
<a
|
||||
class="c-thumb__image-wrapper"
|
||||
href=""
|
||||
:download="image.imageDownloadName"
|
||||
@click.prevent
|
||||
>
|
||||
<img
|
||||
ref="img"
|
||||
class="c-thumb__image"
|
||||
:src="image.url"
|
||||
fetchpriority="low"
|
||||
@load="imageLoadCompleted"
|
||||
>
|
||||
</a>
|
||||
<div
|
||||
v-if="viewableArea"
|
||||
class="c-thumb__viewable-area"
|
||||
:style="viewableAreaStyle"
|
||||
></div>
|
||||
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const THUMB_PADDING = 4;
|
||||
const BORDER_WIDTH = 2;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
image: {
|
||||
@ -63,6 +74,77 @@ export default {
|
||||
realTime: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
viewableArea: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
imgWidth: 0,
|
||||
imgHeight: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
viewableAreaStyle() {
|
||||
if (!this.viewableArea || !this.imgWidth || !this.imgHeight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { widthRatio, heightRatio, xOffsetRatio, yOffsetRatio } = this.viewableArea;
|
||||
const imgWidth = this.imgWidth;
|
||||
const imgHeight = this.imgHeight;
|
||||
|
||||
let translateX = imgWidth * xOffsetRatio;
|
||||
let translateY = imgHeight * yOffsetRatio;
|
||||
let width = imgWidth * widthRatio;
|
||||
let height = imgHeight * heightRatio;
|
||||
|
||||
if (translateX < 0) {
|
||||
width += translateX;
|
||||
translateX = 0;
|
||||
}
|
||||
|
||||
if (translateX + width > imgWidth) {
|
||||
width = imgWidth - translateX;
|
||||
}
|
||||
|
||||
if (translateX + 2 * BORDER_WIDTH > imgWidth) {
|
||||
translateX = imgWidth - 2 * BORDER_WIDTH;
|
||||
}
|
||||
|
||||
if (translateY < 0) {
|
||||
height += translateY;
|
||||
translateY = 0;
|
||||
}
|
||||
|
||||
if (translateY + height > imgHeight) {
|
||||
height = imgHeight - translateY;
|
||||
}
|
||||
|
||||
if (translateY + 2 * BORDER_WIDTH > imgHeight) {
|
||||
translateY = imgHeight - 2 * BORDER_WIDTH;
|
||||
}
|
||||
|
||||
return {
|
||||
'transform': `translate(${translateX + THUMB_PADDING}px, ${translateY + THUMB_PADDING}px)`,
|
||||
'width': `${width}px`,
|
||||
'height': `${height}px`
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
imageLoadCompleted() {
|
||||
if (!this.$refs.img) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width: imgWidth, height: imgHeight } = this.$refs.img;
|
||||
this.imgWidth = imgWidth;
|
||||
this.imgHeight = imgHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -25,7 +25,7 @@
|
||||
tabindex="0"
|
||||
class="c-imagery"
|
||||
@keyup="arrowUpHandler"
|
||||
@keydown="arrowDownHandler"
|
||||
@keydown.prevent="arrowDownHandler"
|
||||
@mouseover="focusElement"
|
||||
>
|
||||
<div
|
||||
@ -147,7 +147,7 @@
|
||||
v-if="!isFixed"
|
||||
class="c-button icon-pause pause-play"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@click="paused(!isPaused)"
|
||||
@click="handlePauseButton(!isPaused)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
@ -165,6 +165,9 @@
|
||||
<div
|
||||
ref="thumbsWrapper"
|
||||
class="c-imagery__thumbs-scroll-area"
|
||||
:class="[{
|
||||
'animate-scroll': animateThumbScroll
|
||||
}]"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<ImageThumbnail
|
||||
@ -174,6 +177,7 @@
|
||||
:active="focusedImageIndex === index"
|
||||
:selected="focusedImageIndex === index && isPaused"
|
||||
:real-time="!isFixed"
|
||||
:viewable-area="focusedImageIndex === index ? viewableArea : null"
|
||||
@click.native="thumbnailClicked(index)"
|
||||
/>
|
||||
</div>
|
||||
@ -181,7 +185,7 @@
|
||||
<button
|
||||
class="c-imagery__auto-scroll-resume-button c-icon-button icon-play"
|
||||
title="Resume automatic scrolling of image thumbnails"
|
||||
@click="scrollToRight('reset')"
|
||||
@click="scrollToRight"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
@ -191,6 +195,7 @@
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import Vue from 'vue';
|
||||
|
||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
@ -219,6 +224,8 @@ const ZOOM_SCALE_DEFAULT = 1;
|
||||
const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
|
||||
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
|
||||
|
||||
const IMAGE_CONTAINER_BORDER_WIDTH = 1;
|
||||
|
||||
export default {
|
||||
name: 'ImageryView',
|
||||
components: {
|
||||
@ -281,10 +288,13 @@ export default {
|
||||
},
|
||||
imageTranslateX: 0,
|
||||
imageTranslateY: 0,
|
||||
imageViewportWidth: 0,
|
||||
imageViewportHeight: 0,
|
||||
pan: undefined,
|
||||
animateZoom: true,
|
||||
imagePanned: false,
|
||||
forceShowThumbnails: false
|
||||
forceShowThumbnails: false,
|
||||
animateThumbScroll: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -388,6 +398,12 @@ export default {
|
||||
|
||||
return disabled;
|
||||
},
|
||||
isComposedInLayout() {
|
||||
return (
|
||||
this.currentView?.objectPath
|
||||
&& !this.openmct.router.isNavigatedObject(this.currentView.objectPath)
|
||||
);
|
||||
},
|
||||
focusedImage() {
|
||||
return this.imageHistory[this.focusedImageIndex];
|
||||
},
|
||||
@ -516,11 +532,28 @@ export default {
|
||||
}
|
||||
|
||||
return 'Alt drag to pan';
|
||||
},
|
||||
viewableArea() {
|
||||
if (this.zoomFactor === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageWidth = this.sizedImageWidth * this.zoomFactor;
|
||||
const imageHeight = this.sizedImageHeight * this.zoomFactor;
|
||||
const xOffset = (imageWidth - this.imageViewportWidth) / 2;
|
||||
const yOffset = (imageHeight - this.imageViewportHeight) / 2;
|
||||
|
||||
return {
|
||||
widthRatio: this.imageViewportWidth / imageWidth,
|
||||
heightRatio: this.imageViewportHeight / imageHeight,
|
||||
xOffsetRatio: (xOffset - this.imageTranslateX * this.zoomFactor) / imageWidth,
|
||||
yOffsetRatio: (yOffset - this.imageTranslateY * this.zoomFactor) / imageHeight
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imageHistory: {
|
||||
handler(newHistory, _oldHistory) {
|
||||
async handler(newHistory, oldHistory) {
|
||||
const newSize = newHistory.length;
|
||||
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
|
||||
if (this.focusedImageTimestamp !== undefined) {
|
||||
@ -548,10 +581,13 @@ export default {
|
||||
|
||||
if (!this.isPaused) {
|
||||
this.setFocusedImage(imageIndex);
|
||||
this.scrollToRight();
|
||||
} else {
|
||||
this.scrollToFocused();
|
||||
}
|
||||
|
||||
await this.scrollHandler();
|
||||
if (oldHistory?.length > 0) {
|
||||
this.animateThumbScroll = true;
|
||||
}
|
||||
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
@ -562,7 +598,7 @@ export default {
|
||||
this.getImageNaturalDimensions();
|
||||
},
|
||||
bounds() {
|
||||
this.scrollToFocused();
|
||||
this.scrollHandler();
|
||||
},
|
||||
isFixed(newValue) {
|
||||
const isRealTime = !newValue;
|
||||
@ -752,7 +788,7 @@ export default {
|
||||
}
|
||||
},
|
||||
persistVisibleLayers() {
|
||||
if (this.domainObject.configuration) {
|
||||
if (this.domainObject.configuration && this.openmct.objects.supportsMutation(this.domainObject.identifier)) {
|
||||
this.openmct.objects.mutate(this.domainObject, 'configuration.layers', this.layers);
|
||||
}
|
||||
|
||||
@ -826,6 +862,13 @@ export default {
|
||||
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
|
||||
this.autoScroll = !disableScroll;
|
||||
},
|
||||
handlePauseButton(newState) {
|
||||
this.paused(newState);
|
||||
if (newState) {
|
||||
// need to set the focused index or the paused focus will drift
|
||||
this.thumbnailClicked(this.focusedImageIndex);
|
||||
}
|
||||
},
|
||||
paused(state) {
|
||||
this.isPaused = Boolean(state);
|
||||
|
||||
@ -833,7 +876,7 @@ export default {
|
||||
this.previousFocusedImage = null;
|
||||
this.setFocusedImage(this.nextImageIndex);
|
||||
this.autoScroll = true;
|
||||
this.scrollToRight();
|
||||
this.scrollHandler();
|
||||
}
|
||||
},
|
||||
scrollToFocused() {
|
||||
@ -843,28 +886,43 @@ export default {
|
||||
}
|
||||
|
||||
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
|
||||
|
||||
if (domThumb) {
|
||||
domThumb.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
}
|
||||
},
|
||||
scrollToRight(type) {
|
||||
if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
|
||||
if (!domThumb) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollWidth = this.$refs.thumbsWrapper.scrollWidth || 0;
|
||||
// separate scrollTo function had to be implemented since scrollIntoView
|
||||
// caused undesirable behavior in layouts
|
||||
// and could not simply be scoped to the parent element
|
||||
if (this.isComposedInLayout) {
|
||||
const wrapperWidth = this.$refs.thumbsWrapper.clientWidth ?? 0;
|
||||
this.$refs.thumbsWrapper.scrollLeft = (
|
||||
domThumb.offsetLeft - (wrapperWidth - domThumb.clientWidth) / 2);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
domThumb.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
},
|
||||
async scrollToRight() {
|
||||
|
||||
const scrollWidth = this.$refs?.thumbsWrapper?.scrollWidth ?? 0;
|
||||
if (!scrollWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
|
||||
});
|
||||
await Vue.nextTick();
|
||||
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
|
||||
},
|
||||
scrollHandler() {
|
||||
if (this.isPaused) {
|
||||
return this.scrollToFocused();
|
||||
} else if (this.autoScroll) {
|
||||
return this.scrollToRight();
|
||||
}
|
||||
},
|
||||
matchIndexOfPreviousImage(previous, imageHistory) {
|
||||
// match logic uses a composite of url and time to account
|
||||
@ -1063,12 +1121,12 @@ export default {
|
||||
}
|
||||
|
||||
this.setSizedImageDimensions();
|
||||
this.setImageViewport();
|
||||
this.calculateViewHeight();
|
||||
this.scrollToFocused();
|
||||
this.scrollHandler();
|
||||
},
|
||||
setSizedImageDimensions() {
|
||||
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
|
||||
|
||||
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
|
||||
// container is wider than image
|
||||
this.sizedImageWidth = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
|
||||
@ -1079,6 +1137,17 @@ export default {
|
||||
this.sizedImageHeight = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;
|
||||
}
|
||||
},
|
||||
setImageViewport() {
|
||||
if (this.imageContainerHeight > this.sizedImageHeight + IMAGE_CONTAINER_BORDER_WIDTH) {
|
||||
// container is taller than wrapper
|
||||
this.imageViewportWidth = this.sizedImageWidth;
|
||||
this.imageViewportHeight = this.sizedImageHeight;
|
||||
} else {
|
||||
// container is wider than wrapper
|
||||
this.imageViewportWidth = this.imageContainerWidth;
|
||||
this.imageViewportHeight = this.imageContainerHeight;
|
||||
}
|
||||
},
|
||||
handleThumbWindowResizeStart() {
|
||||
if (!this.autoScroll) {
|
||||
return;
|
||||
@ -1089,9 +1158,7 @@ export default {
|
||||
this.handleThumbWindowResizeEnded();
|
||||
},
|
||||
handleThumbWindowResizeEnded() {
|
||||
if (!this.isPaused) {
|
||||
this.scrollToRight('reset');
|
||||
}
|
||||
this.scrollHandler();
|
||||
|
||||
this.calculateViewHeight();
|
||||
|
||||
@ -1104,7 +1171,6 @@ export default {
|
||||
},
|
||||
wheelZoom(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.$refs.imageControls.wheelZoom(e);
|
||||
},
|
||||
startPan(e) {
|
||||
|
@ -28,6 +28,7 @@ function copyRelatedMetadata(metadata) {
|
||||
return copiedMetadata;
|
||||
}
|
||||
|
||||
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
|
||||
export default class RelatedTelemetry {
|
||||
|
||||
constructor(openmct, domainObject, telemetryKeys) {
|
||||
@ -88,9 +89,31 @@ export default class RelatedTelemetry {
|
||||
this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId);
|
||||
|
||||
this[key].requestLatestFor = async (datum) => {
|
||||
const options = {
|
||||
// We need to create a throwaway time context and pass it along
|
||||
// as a request option. We do this to "trick" the Time API
|
||||
// into thinking we are in fixed time mode in order to bypass this logic:
|
||||
// https://github.com/akhenry/openmct-yamcs/blob/1060d42ebe43bf346dac0f6a8068cb288ade4ba4/src/providers/historical-telemetry-provider.js#L59
|
||||
// Context: https://github.com/akhenry/openmct-yamcs/pull/217
|
||||
const ephemeralContext = new IndependentTimeContext(
|
||||
this._openmct,
|
||||
this._openmct.time,
|
||||
[this[key].historicalDomainObject]
|
||||
);
|
||||
|
||||
// Stop following the global context, stop the clock,
|
||||
// and set bounds.
|
||||
ephemeralContext.resetContext();
|
||||
const newBounds = {
|
||||
start: this._openmct.time.bounds().start,
|
||||
end: this._parseTime(datum),
|
||||
end: this._parseTime(datum)
|
||||
};
|
||||
ephemeralContext.stopClock();
|
||||
ephemeralContext.bounds(newBounds);
|
||||
|
||||
const options = {
|
||||
start: newBounds.start,
|
||||
end: newBounds.end,
|
||||
timeContext: ephemeralContext,
|
||||
strategy: 'latest'
|
||||
};
|
||||
let results = await this._openmct.telemetry
|
||||
|
@ -194,6 +194,9 @@
|
||||
overflow-y: hidden;
|
||||
margin-bottom: 1px;
|
||||
padding-bottom: $interiorMarginSm;
|
||||
&.animate-scroll {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
&__auto-scroll-resume-button {
|
||||
@ -285,6 +288,13 @@
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
&__viewable-area {
|
||||
position: absolute;
|
||||
border: 2px yellow solid;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-small-thumbs {
|
||||
|
@ -481,19 +481,16 @@ describe("The Imagery View Layouts", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
|
||||
Vue.nextTick(() => {
|
||||
// use spyon to spy the scroll function
|
||||
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
|
||||
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
||||
Vue.nextTick(() => {
|
||||
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
|
||||
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it ('scrollToRight is called when clicking on auto scroll button', async () => {
|
||||
await Vue.nextTick();
|
||||
// use spyon to spy the scroll function
|
||||
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollHandler');
|
||||
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
||||
await Vue.nextTick();
|
||||
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
|
||||
expect(imageryView._getInstance().$refs.ImageryContainer.scrollHandler);
|
||||
});
|
||||
xit('should change the image zoom factor when using the zoom buttons', async (done) => {
|
||||
xit('should change the image zoom factor when using the zoom buttons', async () => {
|
||||
await Vue.nextTick();
|
||||
let imageSizeBefore;
|
||||
let imageSizeAfter;
|
||||
@ -512,7 +509,6 @@ describe("The Imagery View Layouts", () => {
|
||||
imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
|
||||
expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);
|
||||
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
|
||||
done();
|
||||
});
|
||||
xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {
|
||||
await Vue.nextTick();
|
||||
@ -529,6 +525,19 @@ describe("The Imagery View Layouts", () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should display the viewable area when zoom factor is greater than 1', async () => {
|
||||
await Vue.nextTick();
|
||||
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);
|
||||
|
||||
parent.querySelector('.t-btn-zoom-in').click();
|
||||
await Vue.nextTick();
|
||||
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(1);
|
||||
|
||||
parent.querySelector('.t-btn-zoom-reset').click();
|
||||
await Vue.nextTick();
|
||||
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should reset the brightness and contrast when clicking the reset button', async () => {
|
||||
const viewInstance = imageryView._getInstance();
|
||||
await Vue.nextTick();
|
||||
|
@ -50,7 +50,7 @@
|
||||
<Sidebar
|
||||
ref="sidebar"
|
||||
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
|
||||
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
|
||||
:class="sidebarClasses"
|
||||
:default-page-id="defaultPageId"
|
||||
:selected-page-id="getSelectedPageId()"
|
||||
:default-section-id="defaultSectionId"
|
||||
@ -123,6 +123,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedPage && !selectedPage.isLocked"
|
||||
:class="{ 'disabled': activeTransaction }"
|
||||
class="c-notebook__drag-area icon-plus"
|
||||
@click="newEntry()"
|
||||
@dragover="dragOver"
|
||||
@ -133,6 +134,11 @@
|
||||
To start a new entry, click here or drag and drop any object
|
||||
</span>
|
||||
</div>
|
||||
<progress-bar
|
||||
v-if="savingTransaction"
|
||||
class="c-telemetry-table__progress-bar"
|
||||
:model="{ progressPerc: undefined }"
|
||||
/>
|
||||
<div
|
||||
v-if="selectedPage && selectedPage.isLocked"
|
||||
class="c-notebook__page-locked"
|
||||
@ -183,6 +189,7 @@ import NotebookEntry from './NotebookEntry.vue';
|
||||
import Search from '@/ui/components/search.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
import Sidebar from './Sidebar.vue';
|
||||
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
||||
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
|
||||
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
||||
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
||||
@ -200,7 +207,8 @@ export default {
|
||||
NotebookEntry,
|
||||
Search,
|
||||
SearchResults,
|
||||
Sidebar
|
||||
Sidebar,
|
||||
ProgressBar
|
||||
},
|
||||
inject: ['agent', 'openmct', 'snapshotContainer'],
|
||||
props: {
|
||||
@ -225,7 +233,9 @@ export default {
|
||||
showNav: false,
|
||||
sidebarCoversEntries: false,
|
||||
filteredAndSortedEntries: [],
|
||||
notebookAnnotations: {}
|
||||
notebookAnnotations: {},
|
||||
activeTransaction: false,
|
||||
savingTransaction: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -270,6 +280,20 @@ export default {
|
||||
|
||||
return this.sections[0];
|
||||
},
|
||||
sidebarClasses() {
|
||||
let sidebarClasses = [];
|
||||
if (this.showNav) {
|
||||
sidebarClasses.push('is-expanded');
|
||||
}
|
||||
|
||||
if (this.sidebarCoversEntries) {
|
||||
sidebarClasses.push('c-drawer--overlays');
|
||||
} else {
|
||||
sidebarClasses.push('c-drawer--push');
|
||||
}
|
||||
|
||||
return sidebarClasses;
|
||||
},
|
||||
showLockButton() {
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
|
||||
@ -297,6 +321,8 @@ export default {
|
||||
this.formatSidebar();
|
||||
this.setSectionAndPageFromUrl();
|
||||
|
||||
this.transaction = null;
|
||||
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.filterAndSortEntries();
|
||||
@ -749,6 +775,7 @@ export default {
|
||||
return section.id;
|
||||
},
|
||||
async newEntry(embed = null) {
|
||||
this.startTransaction();
|
||||
this.resetSearch();
|
||||
const notebookStorage = this.createNotebookStorageObject();
|
||||
this.updateDefaultNotebook(notebookStorage);
|
||||
@ -891,20 +918,34 @@ export default {
|
||||
},
|
||||
startTransaction() {
|
||||
if (!this.openmct.objects.isTransactionActive()) {
|
||||
this.activeTransaction = true;
|
||||
this.transaction = this.openmct.objects.startTransaction();
|
||||
}
|
||||
},
|
||||
async saveTransaction() {
|
||||
if (this.transaction !== undefined) {
|
||||
await this.transaction.commit();
|
||||
this.openmct.objects.endTransaction();
|
||||
if (this.transaction !== null) {
|
||||
this.savingTransaction = true;
|
||||
try {
|
||||
await this.transaction.commit();
|
||||
} finally {
|
||||
this.endTransaction();
|
||||
}
|
||||
}
|
||||
},
|
||||
async cancelTransaction() {
|
||||
if (this.transaction !== undefined) {
|
||||
await this.transaction.cancel();
|
||||
this.openmct.objects.endTransaction();
|
||||
if (this.transaction !== null) {
|
||||
try {
|
||||
await this.transaction.cancel();
|
||||
} finally {
|
||||
this.endTransaction();
|
||||
}
|
||||
}
|
||||
},
|
||||
endTransaction() {
|
||||
this.openmct.objects.endTransaction();
|
||||
this.transaction = null;
|
||||
this.savingTransaction = false;
|
||||
this.activeTransaction = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -74,19 +74,22 @@ async function resolveNotebookTagConflicts(localAnnotation, openmct) {
|
||||
|
||||
async function resolveNotebookEntryConflicts(localMutable, openmct) {
|
||||
if (localMutable.configuration.entries) {
|
||||
const FORCE_REMOTE = true;
|
||||
const localEntries = structuredClone(localMutable.configuration.entries);
|
||||
const remoteMutable = await openmct.objects.getMutable(localMutable.identifier);
|
||||
applyLocalEntries(remoteMutable, localEntries, openmct);
|
||||
openmct.objects.destroyMutable(remoteMutable);
|
||||
const remoteObject = await openmct.objects.get(localMutable.identifier, undefined, FORCE_REMOTE);
|
||||
|
||||
return applyLocalEntries(remoteObject, localEntries, openmct);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyLocalEntries(mutable, entries, openmct) {
|
||||
function applyLocalEntries(remoteObject, entries, openmct) {
|
||||
let shouldSave = false;
|
||||
|
||||
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
|
||||
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
|
||||
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
|
||||
const remoteEntries = remoteObject.configuration.entries[sectionKey][pageKey];
|
||||
const mergedEntries = [].concat(remoteEntries);
|
||||
let shouldMutate = false;
|
||||
|
||||
@ -110,8 +113,13 @@ function applyLocalEntries(mutable, entries, openmct) {
|
||||
});
|
||||
|
||||
if (shouldMutate) {
|
||||
openmct.objects.mutate(mutable, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
|
||||
shouldSave = true;
|
||||
openmct.objects.mutate(remoteObject, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (shouldSave) {
|
||||
return openmct.objects.save(remoteObject);
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,8 @@ export default function () {
|
||||
}
|
||||
|
||||
let wrappedFunction = openmct.objects.get;
|
||||
openmct.objects.get = function migrate(identifier) {
|
||||
return wrappedFunction.apply(openmct.objects, [identifier])
|
||||
openmct.objects.get = function migrate() {
|
||||
return wrappedFunction.apply(openmct.objects, [...arguments])
|
||||
.then(function (object) {
|
||||
if (needsMigration(object)) {
|
||||
migrateObject(object)
|
||||
|
@ -31,8 +31,8 @@ export default class OpenInNewTab {
|
||||
|
||||
this._openmct = openmct;
|
||||
}
|
||||
invoke(objectPath) {
|
||||
let url = objectPathToUrl(this._openmct, objectPath);
|
||||
invoke(objectPath, urlParams = undefined) {
|
||||
let url = objectPathToUrl(this._openmct, objectPath, urlParams);
|
||||
window.open(url);
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@
|
||||
connected = false;
|
||||
// stop listening for events
|
||||
couchEventSource.removeEventListener('message', self.onCouchMessage);
|
||||
couchEventSource.close();
|
||||
console.debug('🚪 Closed couch connection 🚪');
|
||||
|
||||
return;
|
||||
|
@ -96,8 +96,13 @@ class CouchObjectProvider {
|
||||
let keyString = this.openmct.objects.makeKeyString(objectIdentifier);
|
||||
//TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have.
|
||||
let observersForObject = this.observers[keyString];
|
||||
let isInTransaction = false;
|
||||
|
||||
if (observersForObject) {
|
||||
if (this.openmct.objects.isTransactionActive()) {
|
||||
isInTransaction = this.openmct.objects.transaction.getDirtyObject(objectIdentifier);
|
||||
}
|
||||
|
||||
if (observersForObject && !isInTransaction) {
|
||||
observersForObject.forEach(async (observer) => {
|
||||
const updatedObject = await this.get(objectIdentifier);
|
||||
if (this.isSynchronizedObject(updatedObject)) {
|
||||
@ -219,7 +224,12 @@ class CouchObjectProvider {
|
||||
console.error(error.message);
|
||||
throw new Error(`CouchDB Error - No response"`);
|
||||
} else {
|
||||
console.error(error.message);
|
||||
if (body?.model && isNotebookOrAnnotationType(body.model)) {
|
||||
// warn since we handle conflicts for notebooks
|
||||
console.warn(error.message);
|
||||
} else {
|
||||
console.error(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
@ -234,7 +244,8 @@ class CouchObjectProvider {
|
||||
#handleResponseCode(status, json, fetchOptions) {
|
||||
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
|
||||
if (status === CouchObjectProvider.HTTP_CONFLICT) {
|
||||
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
|
||||
const objectName = JSON.parse(fetchOptions.body)?.model?.name;
|
||||
throw new this.openmct.objects.errors.Conflict(`Conflict persisting "${objectName}"`);
|
||||
} else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {
|
||||
if (!json.error || !json.reason) {
|
||||
throw new Error(`CouchDB Error ${status}`);
|
||||
|
@ -383,10 +383,8 @@ export default {
|
||||
},
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||
this.followTimeContext();
|
||||
|
||||
},
|
||||
followTimeContext() {
|
||||
this.updateDisplayBounds(this.timeContext.bounds());
|
||||
|
@ -83,6 +83,8 @@ export default class PlotSeries extends Model {
|
||||
// Model.apply(this, arguments);
|
||||
this.onXKeyChange(this.get('xKey'));
|
||||
this.onYKeyChange(this.get('yKey'));
|
||||
|
||||
this.unPlottableValues = [undefined, Infinity, -Infinity];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -342,6 +344,10 @@ export default class PlotSeries extends Model {
|
||||
let stats = this.get('stats');
|
||||
let changed = false;
|
||||
if (!stats) {
|
||||
if ([Infinity, -Infinity].includes(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
stats = {
|
||||
minValue: value,
|
||||
minPoint: point,
|
||||
@ -350,13 +356,13 @@ export default class PlotSeries extends Model {
|
||||
};
|
||||
changed = true;
|
||||
} else {
|
||||
if (stats.maxValue < value) {
|
||||
if (stats.maxValue < value && value !== Infinity) {
|
||||
stats.maxValue = value;
|
||||
stats.maxPoint = point;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (stats.minValue > value) {
|
||||
if (stats.minValue > value && value !== -Infinity) {
|
||||
stats.minValue = value;
|
||||
stats.minPoint = point;
|
||||
changed = true;
|
||||
@ -419,7 +425,7 @@ export default class PlotSeries extends Model {
|
||||
* @private
|
||||
*/
|
||||
isValueInvalid(val) {
|
||||
return Number.isNaN(val) || val === undefined;
|
||||
return Number.isNaN(val) || this.unPlottableValues.includes(val);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,8 +62,8 @@ export default class RemoteClock extends DefaultClock {
|
||||
}
|
||||
|
||||
start() {
|
||||
this.openmct.time.on('timeSystem', this._timeSystemChange);
|
||||
this.openmct.objects.get(this.identifier).then((domainObject) => {
|
||||
this.openmct.time.on('timeSystem', this._timeSystemChange);
|
||||
this.timeTelemetryObject = domainObject;
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
this._timeSystemChange();
|
||||
|
@ -71,7 +71,10 @@ describe("the RemoteClock plugin", () => {
|
||||
parse: (datum) => datum.key
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
let objectPromise;
|
||||
let requestPromise;
|
||||
|
||||
beforeEach(() => {
|
||||
openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID));
|
||||
|
||||
let clocks = openmct.time.getAllClocks();
|
||||
@ -89,7 +92,9 @@ describe("the RemoteClock plugin", () => {
|
||||
spyOn(metadata, 'value').and.callThrough();
|
||||
|
||||
let requestPromiseResolve;
|
||||
let requestPromise = new Promise((resolve) => {
|
||||
let objectPromiseResolve;
|
||||
|
||||
requestPromise = new Promise((resolve) => {
|
||||
requestPromiseResolve = resolve;
|
||||
});
|
||||
spyOn(openmct.telemetry, 'request').and.callFake(() => {
|
||||
@ -98,8 +103,7 @@ describe("the RemoteClock plugin", () => {
|
||||
return requestPromise;
|
||||
});
|
||||
|
||||
let objectPromiseResolve;
|
||||
let objectPromise = new Promise((resolve) => {
|
||||
objectPromise = new Promise((resolve) => {
|
||||
objectPromiseResolve = resolve;
|
||||
});
|
||||
spyOn(openmct.objects, 'get').and.callFake(() => {
|
||||
@ -112,39 +116,48 @@ describe("the RemoteClock plugin", () => {
|
||||
start: OFFSET_START,
|
||||
end: OFFSET_END
|
||||
});
|
||||
|
||||
await Promise.all([objectPromiseResolve, requestPromise]);
|
||||
});
|
||||
|
||||
it('is available and sets up initial values and listeners', () => {
|
||||
expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);
|
||||
expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);
|
||||
expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);
|
||||
expect(remoteClock._timeSystemChange).toHaveBeenCalled();
|
||||
it("Does not throw error if time system is changed before remote clock initialized", () => {
|
||||
expect(() => openmct.time.timeSystem('utc')).not.toThrow();
|
||||
});
|
||||
|
||||
it('will request/store the object based on the identifier passed in', () => {
|
||||
expect(remoteClock.timeTelemetryObject).toEqual(object);
|
||||
describe('once resolved', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all([objectPromise, requestPromise]);
|
||||
});
|
||||
|
||||
it('is available and sets up initial values and listeners', () => {
|
||||
expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);
|
||||
expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);
|
||||
expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);
|
||||
expect(remoteClock._timeSystemChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('will request/store the object based on the identifier passed in', () => {
|
||||
expect(remoteClock.timeTelemetryObject).toEqual(object);
|
||||
});
|
||||
|
||||
it('will request metadata and set up formatters', () => {
|
||||
expect(remoteClock.metadata).toEqual(metadata);
|
||||
expect(metadata.value).toHaveBeenCalled();
|
||||
expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);
|
||||
});
|
||||
|
||||
it('will request the latest datum for the object it received and process the datum returned', () => {
|
||||
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
|
||||
expect(boundsCallback).toHaveBeenCalledWith({
|
||||
start: TIME_VALUE + OFFSET_START,
|
||||
end: TIME_VALUE + OFFSET_END
|
||||
}, true);
|
||||
});
|
||||
|
||||
it('will set up subscriptions correctly', () => {
|
||||
expect(remoteClock._unsubscribe).toBeDefined();
|
||||
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum);
|
||||
});
|
||||
});
|
||||
|
||||
it('will request metadata and set up formatters', () => {
|
||||
expect(remoteClock.metadata).toEqual(metadata);
|
||||
expect(metadata.value).toHaveBeenCalled();
|
||||
expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);
|
||||
});
|
||||
|
||||
it('will request the latest datum for the object it received and process the datum returned', () => {
|
||||
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
|
||||
expect(boundsCallback).toHaveBeenCalledWith({
|
||||
start: TIME_VALUE + OFFSET_START,
|
||||
end: TIME_VALUE + OFFSET_END
|
||||
}, true);
|
||||
});
|
||||
|
||||
it('will set up subscriptions correctly', () => {
|
||||
expect(remoteClock._unsubscribe).toBeDefined();
|
||||
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -178,7 +178,7 @@ define([
|
||||
if (this.paused) {
|
||||
this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));
|
||||
} else {
|
||||
this.tableRows.addRows(telemetryRows, 'add');
|
||||
this.tableRows.addRows(telemetryRows);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -229,7 +229,7 @@ define([
|
||||
});
|
||||
});
|
||||
|
||||
this.tableRows.addRows(allRows, 'filter');
|
||||
this.tableRows.clearRowsFromTableAndFilter(allRows);
|
||||
}
|
||||
|
||||
updateFilters(updatedFilters) {
|
||||
|
@ -61,30 +61,39 @@ define(
|
||||
this.emit('remove', removed);
|
||||
}
|
||||
|
||||
addRows(rows, type = 'add') {
|
||||
if (this.sortOptions === undefined) {
|
||||
throw 'Please specify sort options';
|
||||
}
|
||||
|
||||
let isFilterTriggeredReset = type === 'filter';
|
||||
let anyActiveFilters = Object.keys(this.columnFilters).length > 0;
|
||||
let rowsToAdd = !anyActiveFilters ? rows : rows.filter(this.matchesFilters, this);
|
||||
|
||||
// if type is filter, then it's a reset of all rows,
|
||||
// need to wipe current rows
|
||||
if (isFilterTriggeredReset) {
|
||||
this.rows = [];
|
||||
}
|
||||
addRows(rows) {
|
||||
let rowsToAdd = this.filterRows(rows);
|
||||
|
||||
this.sortAndMergeRows(rowsToAdd);
|
||||
|
||||
// we emit filter no matter what to trigger
|
||||
// an update of visible rows
|
||||
if (rowsToAdd.length > 0 || isFilterTriggeredReset) {
|
||||
this.emit(type, rowsToAdd);
|
||||
if (rowsToAdd.length > 0) {
|
||||
this.emit('add', rowsToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
clearRowsFromTableAndFilter(rows) {
|
||||
|
||||
let rowsToAdd = this.filterRows(rows);
|
||||
// Reset of all rows, need to wipe current rows
|
||||
this.rows = [];
|
||||
|
||||
this.sortAndMergeRows(rowsToAdd);
|
||||
|
||||
// We emit filter and update of visible rows
|
||||
this.emit('filter', rowsToAdd);
|
||||
}
|
||||
|
||||
filterRows(rows) {
|
||||
|
||||
if (Object.keys(this.columnFilters).length > 0) {
|
||||
return rows.filter(this.matchesFilters, this);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
sortAndMergeRows(rows) {
|
||||
const sortedRowsToAdd = this.sortCollection(rows);
|
||||
|
||||
|
@ -24,9 +24,27 @@
|
||||
* Module defining url handling.
|
||||
*/
|
||||
|
||||
export function paramsToArray(openmct) {
|
||||
// parse urlParams from an object to an array.
|
||||
function getUrlParams(openmct, customUrlParams = {}) {
|
||||
let urlParams = openmct.router.getParams();
|
||||
Object.entries(customUrlParams).forEach((urlParam) => {
|
||||
const [key, value] = urlParam;
|
||||
urlParams[key] = value;
|
||||
});
|
||||
|
||||
if (urlParams['tc.mode'] === 'fixed') {
|
||||
delete urlParams['tc.startDelta'];
|
||||
delete urlParams['tc.endDelta'];
|
||||
} else if (urlParams['tc.mode'] === 'local') {
|
||||
delete urlParams['tc.startBound'];
|
||||
delete urlParams['tc.endBound'];
|
||||
}
|
||||
|
||||
return urlParams;
|
||||
}
|
||||
|
||||
export function paramsToArray(openmct, customUrlParams = {}) {
|
||||
// parse urlParams from an object to an array.
|
||||
let urlParams = getUrlParams(openmct, customUrlParams);
|
||||
let newTabParams = [];
|
||||
for (let key in urlParams) {
|
||||
if ({}.hasOwnProperty.call(urlParams, key)) {
|
||||
@ -42,9 +60,9 @@ export function identifierToString(openmct, objectPath) {
|
||||
return '#/browse/' + openmct.objects.getRelativePath(objectPath);
|
||||
}
|
||||
|
||||
export default function objectPathToUrl(openmct, objectPath) {
|
||||
export default function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
|
||||
let url = identifierToString(openmct, objectPath);
|
||||
let urlParams = paramsToArray(openmct);
|
||||
let urlParams = paramsToArray(openmct, customUrlParams);
|
||||
if (urlParams.length) {
|
||||
url += '?' + urlParams.join('&');
|
||||
}
|
||||
|
@ -66,5 +66,14 @@ describe('the url tool', function () {
|
||||
const constructedURL = objectPathToUrl(openmct, mockObjectPath);
|
||||
expect(constructedURL).toContain('#/browse/mock-parent-folder/mock-folder');
|
||||
});
|
||||
it('can take params to set a custom url', () => {
|
||||
const customParams = {
|
||||
'tc.startBound': 1669911059,
|
||||
'tc.endBound': 1669911082,
|
||||
'tc.mode': 'fixed'
|
||||
};
|
||||
const constructedURL = objectPathToUrl(openmct, mockObjectPath, customParams);
|
||||
expect(constructedURL).toContain('tc.startBound=1669911059&tc.endBound=1669911082&tc.mode=fixed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -101,7 +101,8 @@ export default {
|
||||
if (nowMarker) {
|
||||
nowMarker.classList.remove('hidden');
|
||||
nowMarker.style.height = this.contentHeight + 'px';
|
||||
const now = this.xScale(Date.now());
|
||||
const nowTimeStamp = this.openmct.time.clock().currentValue();
|
||||
const now = this.xScale(nowTimeStamp);
|
||||
nowMarker.style.left = now + this.offset + 'px';
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,11 @@
|
||||
:checked="checked"
|
||||
@change="onUserSelect($event)"
|
||||
>
|
||||
<span class="c-toggle-switch__slider"></span>
|
||||
<span
|
||||
class="c-toggle-switch__slider"
|
||||
role="switch"
|
||||
:aria-label="name"
|
||||
></span>
|
||||
</label>
|
||||
<div
|
||||
v-if="label && label.length"
|
||||
@ -32,6 +36,11 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
checked: Boolean
|
||||
},
|
||||
methods: {
|
||||
|
@ -22,8 +22,10 @@
|
||||
|
||||
import ObjectView from './ObjectView.vue';
|
||||
import StackedPlot from '../../plugins/plot/stackedPlot/StackedPlot.vue';
|
||||
import Plot from '../../plugins/plot/Plot.vue';
|
||||
|
||||
export default {
|
||||
ObjectView,
|
||||
StackedPlot
|
||||
StackedPlot,
|
||||
Plot
|
||||
};
|
||||
|
@ -335,6 +335,7 @@ export default {
|
||||
dialog.dismiss();
|
||||
this.openmct.notifications.error('Error saving objects');
|
||||
console.error(error);
|
||||
this.openmct.editor.cancel();
|
||||
});
|
||||
},
|
||||
saveAndContinueEditing() {
|
||||
|
@ -76,7 +76,7 @@
|
||||
<div :style="childrenHeightStyles">
|
||||
<tree-item
|
||||
v-for="(treeItem, index) in visibleItems"
|
||||
:key="treeItem.navigationPath"
|
||||
:key="`${treeItem.navigationPath}-${index}`"
|
||||
:node="treeItem"
|
||||
:is-selector-tree="isSelectorTree"
|
||||
:selected-item="selectedItem"
|
||||
@ -174,8 +174,7 @@ export default {
|
||||
itemOffset: 0,
|
||||
activeSearch: false,
|
||||
mainTreeTopMargin: undefined,
|
||||
selectedItem: {},
|
||||
observers: {}
|
||||
selectedItem: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -277,10 +276,13 @@ export default {
|
||||
this.treeResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
this.destroyObservers(this.observers);
|
||||
this.destroyObservers();
|
||||
this.destroyMutables();
|
||||
},
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.observers = {};
|
||||
this.mutables = {};
|
||||
this.isLoading = true;
|
||||
this.getSavedOpenItems();
|
||||
this.treeResizeObserver = new ResizeObserver(this.handleTreeResize);
|
||||
@ -355,8 +357,15 @@ export default {
|
||||
}
|
||||
|
||||
this.treeItems = this.treeItems.filter((checkItem) => {
|
||||
return checkItem.navigationPath === path
|
||||
|| !checkItem.navigationPath.includes(path);
|
||||
if (checkItem.navigationPath !== path
|
||||
&& checkItem.navigationPath.includes(path)) {
|
||||
this.destroyObserverByPath(checkItem.navigationPath);
|
||||
this.destroyMutableByPath(checkItem.navigationPath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
this.openTreeItems.splice(pathIndex, 1);
|
||||
this.removeCompositionListenerFor(path);
|
||||
@ -436,7 +445,17 @@ export default {
|
||||
|
||||
}, Promise.resolve()).then(() => {
|
||||
if (this.isSelectorTree) {
|
||||
this.treeItemSelection(this.getTreeItemByPath(navigationPath));
|
||||
// If item is missing due to error in object creation,
|
||||
// walk up the navigationPath until we find an item
|
||||
let item = this.getTreeItemByPath(navigationPath);
|
||||
while (!item) {
|
||||
const startIndex = 0;
|
||||
const endIndex = navigationPath.lastIndexOf('/');
|
||||
navigationPath = navigationPath.substring(startIndex, endIndex);
|
||||
item = this.getTreeItemByPath(navigationPath);
|
||||
}
|
||||
|
||||
this.treeItemSelection(item);
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -537,7 +556,7 @@ export default {
|
||||
composition = sortedComposition;
|
||||
}
|
||||
|
||||
if (parentObjectPath.length) {
|
||||
if (parentObjectPath.length && !this.isSelectorTree) {
|
||||
let navigationPath = this.buildNavigationPath(parentObjectPath);
|
||||
|
||||
if (this.compositionCollections[navigationPath]) {
|
||||
@ -556,7 +575,15 @@ export default {
|
||||
}
|
||||
|
||||
return composition.map((object) => {
|
||||
this.addTreeItemObserver(object, parentObjectPath);
|
||||
// Only add observers and mutables if this is NOT a selector tree
|
||||
if (!this.isSelectorTree) {
|
||||
if (this.openmct.objects.supportsMutation(object.identifier)) {
|
||||
object = this.openmct.objects.toMutable(object);
|
||||
this.addMutable(object, parentObjectPath);
|
||||
}
|
||||
|
||||
this.addTreeItemObserver(object, parentObjectPath);
|
||||
}
|
||||
|
||||
return this.buildTreeItem(object, parentObjectPath);
|
||||
});
|
||||
@ -574,6 +601,15 @@ export default {
|
||||
navigationPath
|
||||
};
|
||||
},
|
||||
addMutable(mutableDomainObject, parentObjectPath) {
|
||||
const objectPath = [mutableDomainObject].concat(parentObjectPath);
|
||||
const navigationPath = this.buildNavigationPath(objectPath);
|
||||
|
||||
// If the mutable already exists, destroy it.
|
||||
this.destroyMutableByPath(navigationPath);
|
||||
|
||||
this.mutables[navigationPath] = () => this.openmct.objects.destroyMutable(mutableDomainObject);
|
||||
},
|
||||
addTreeItemObserver(domainObject, parentObjectPath) {
|
||||
const objectPath = [domainObject].concat(parentObjectPath);
|
||||
const navigationPath = this.buildNavigationPath(objectPath);
|
||||
@ -588,30 +624,6 @@ export default {
|
||||
this.sortTreeItems.bind(this, parentObjectPath)
|
||||
);
|
||||
},
|
||||
async updateTreeItems(parentObjectPath) {
|
||||
let children;
|
||||
|
||||
if (parentObjectPath.length) {
|
||||
const parentItem = this.treeItems.find(item => item.objectPath === parentObjectPath);
|
||||
const descendants = this.getChildrenInTreeFor(parentItem, true);
|
||||
const parentIndex = this.treeItems.map(e => e.object).indexOf(parentObjectPath[0]);
|
||||
|
||||
children = await this.loadAndBuildTreeItemsFor(parentItem.object, parentItem.objectPath);
|
||||
|
||||
this.treeItems.splice(parentIndex + 1, descendants.length, ...children);
|
||||
} else {
|
||||
const root = await this.openmct.objects.get('ROOT');
|
||||
children = await this.loadAndBuildTreeItemsFor(root, []);
|
||||
|
||||
this.treeItems = [...children];
|
||||
}
|
||||
|
||||
for (let item of children) {
|
||||
if (this.isTreeItemOpen(item)) {
|
||||
this.openTreeItem(item);
|
||||
}
|
||||
}
|
||||
},
|
||||
sortTreeItems(parentObjectPath) {
|
||||
const navigationPath = this.buildNavigationPath(parentObjectPath);
|
||||
const parentItem = this.getTreeItemByPath(navigationPath);
|
||||
@ -662,6 +674,10 @@ export default {
|
||||
const descendants = this.getChildrenInTreeFor(parentItem, true);
|
||||
const directDescendants = this.getChildrenInTreeFor(parentItem);
|
||||
|
||||
if (domainObject.isMutable) {
|
||||
this.addMutable(domainObject, parentItem.objectPath);
|
||||
}
|
||||
|
||||
this.addTreeItemObserver(domainObject, parentItem.objectPath);
|
||||
|
||||
if (directDescendants.length === 0) {
|
||||
@ -692,13 +708,15 @@ export default {
|
||||
},
|
||||
compositionRemoveHandler(navigationPath) {
|
||||
return (identifier) => {
|
||||
let removeKeyString = this.openmct.objects.makeKeyString(identifier);
|
||||
let parentItem = this.getTreeItemByPath(navigationPath);
|
||||
let directDescendants = this.getChildrenInTreeFor(parentItem);
|
||||
let removeItem = directDescendants.find(item => item.id === removeKeyString);
|
||||
const removeKeyString = this.openmct.objects.makeKeyString(identifier);
|
||||
const parentItem = this.getTreeItemByPath(navigationPath);
|
||||
const directDescendants = this.getChildrenInTreeFor(parentItem);
|
||||
const removeItem = directDescendants.find(item => item.id === removeKeyString);
|
||||
|
||||
// Remove the item from the tree, unobserve it, and clean up any mutables
|
||||
this.removeItemFromTree(removeItem);
|
||||
this.removeItemFromObservers(removeItem);
|
||||
this.destroyObserverByPath(removeItem.navigationPath);
|
||||
this.destroyMutableByPath(removeItem.navigationPath);
|
||||
};
|
||||
},
|
||||
removeCompositionListenerFor(navigationPath) {
|
||||
@ -720,13 +738,6 @@ export default {
|
||||
const removeIndex = this.getTreeItemIndex(item.navigationPath);
|
||||
this.treeItems.splice(removeIndex, 1);
|
||||
},
|
||||
removeItemFromObservers(item) {
|
||||
if (this.observers[item.id]) {
|
||||
this.observers[item.id]();
|
||||
|
||||
delete this.observers[item.id];
|
||||
}
|
||||
},
|
||||
addItemToTreeBefore(addItem, beforeItem) {
|
||||
const addIndex = this.getTreeItemIndex(beforeItem.navigationPath);
|
||||
|
||||
@ -792,12 +803,17 @@ export default {
|
||||
|
||||
for (const result of results) {
|
||||
if (!abortSignal.aborted) {
|
||||
// Don't show deleted objects in search results
|
||||
if (result.location === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
resultPromises.push(this.openmct.objects.getOriginalPath(result.identifier).then((objectPath) => {
|
||||
// removing the item itself, as the path we pass to buildTreeItem is a parent path
|
||||
objectPath.shift();
|
||||
|
||||
// if root, remove, we're not using in object path for tree
|
||||
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
|
||||
const lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
|
||||
if (lastObject && lastObject.type === 'root') {
|
||||
objectPath.pop();
|
||||
}
|
||||
@ -959,13 +975,46 @@ export default {
|
||||
handleTreeResize() {
|
||||
this.calculateHeights();
|
||||
},
|
||||
destroyObservers(observers) {
|
||||
Object.entries(observers).forEach(([keyString, unobserve]) => {
|
||||
if (typeof unobserve === 'function') {
|
||||
/**
|
||||
* Destroy an observer for the given navigationPath.
|
||||
*/
|
||||
destroyObserverByPath(navigationPath) {
|
||||
if (this.observers[navigationPath]) {
|
||||
this.observers[navigationPath]();
|
||||
delete this.observers[navigationPath];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Destroy all observers.
|
||||
*/
|
||||
destroyObservers() {
|
||||
Object.entries(this.observers).forEach(([key, unobserve]) => {
|
||||
if (unobserve) {
|
||||
unobserve();
|
||||
}
|
||||
|
||||
delete observers[keyString];
|
||||
delete this.observers[key];
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Destroy a mutable for the given navigationPath.
|
||||
*/
|
||||
destroyMutableByPath(navigationPath) {
|
||||
if (this.mutables[navigationPath]) {
|
||||
this.mutables[navigationPath]();
|
||||
delete this.mutables[navigationPath];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Destroy all mutables.
|
||||
*/
|
||||
destroyMutables() {
|
||||
Object.entries(this.mutables).forEach(([key, destroyMutable]) => {
|
||||
if (destroyMutable) {
|
||||
destroyMutable();
|
||||
}
|
||||
|
||||
delete this.mutables[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -22,5 +22,5 @@ module.exports = merge(common, {
|
||||
__OPENMCT_ROOT_RELATIVE__: '""'
|
||||
})
|
||||
],
|
||||
devtool: 'eval-source-map'
|
||||
devtool: 'source-map'
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user