Compare commits
31 Commits
notebook-c
...
v2.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8e70cd831 | ||
|
|
4958594336 | ||
|
|
a96490da22 | ||
|
|
532cec1531 | ||
|
|
a11a4a23e1 | ||
|
|
1e4d585e9d | ||
|
|
cbecd79f71 | ||
|
|
3deb2e3dc2 | ||
|
|
d6e80447ab | ||
|
|
1a4bd0fb55 | ||
|
|
80f89c7609 | ||
|
|
b82649772f | ||
|
|
7f2ed27106 | ||
|
|
57e02db6b5 | ||
|
|
d54335d21c | ||
|
|
e0ed0bb6e2 | ||
|
|
ed3fd8f965 | ||
|
|
e6d59c61d1 | ||
|
|
b74b27c464 | ||
|
|
d35e161701 | ||
|
|
653cb62f9c | ||
|
|
19b3232fa0 | ||
|
|
19892aab53 | ||
|
|
a168ce25cf | ||
|
|
189c58f952 | ||
|
|
0dfc028e1b | ||
|
|
77e93f1aee | ||
|
|
394fbbe61b | ||
|
|
40afb04f0c | ||
|
|
be73b0158a | ||
|
|
625205f24b |
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
const TWO_HOURS = ONE_HOUR * 2;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
// openmct.install(openmct.plugins.LocalStorage());
|
||||
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
|
||||
openmct.install(openmct.plugins.example.Generator());
|
||||
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
|
||||
|
||||
20
package.json
20
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.4-SNAPSHOT",
|
||||
"version": "2.1.5",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
@@ -8,9 +8,9 @@
|
||||
"@percy/cli": "1.16.0",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.25.2",
|
||||
"@types/jasmine": "4.3.0",
|
||||
"@types/lodash": "4.14.189",
|
||||
"babel-loader": "9.0.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",
|
||||
@@ -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.38",
|
||||
"moment-timezone": "0.5.40",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"playwright-core": "1.25.2",
|
||||
@@ -55,9 +55,9 @@
|
||||
"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.9.3",
|
||||
"typescript": "4.9.4",
|
||||
"uuid": "9.0.0",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "9.1.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>
|
||||
|
||||
@@ -187,17 +187,36 @@ export default class ObjectAPI {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Force remote get for a domain object. Don't return dirty objects.
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @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
|
||||
*/
|
||||
remoteGet(identifier, abortSignal) {
|
||||
const keystring = this.makeKeyString(identifier);
|
||||
get(identifier, abortSignal, forceRemote = false) {
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
|
||||
if (!forceRemote) {
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
let dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
if (!provider) {
|
||||
@@ -210,6 +229,7 @@ export default class ObjectAPI {
|
||||
|
||||
let objectPromise = provider.get(identifier, abortSignal).then(result => {
|
||||
delete this.cache[keystring];
|
||||
|
||||
result = this.applyGetInterceptors(identifier, result);
|
||||
if (result.isMutable) {
|
||||
result.$refresh(result);
|
||||
@@ -234,36 +254,6 @@ export default class ObjectAPI {
|
||||
return objectPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @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
|
||||
* @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) {
|
||||
const keystring = this.makeKeyString(identifier);
|
||||
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
let dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
}
|
||||
}
|
||||
|
||||
return this.remoteGet(identifier, abortSignal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for domain objects.
|
||||
*
|
||||
@@ -369,7 +359,6 @@ export default class ObjectAPI {
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
async save(domainObject) {
|
||||
console.log('save', JSON.parse(JSON.stringify(domainObject)));
|
||||
const provider = this.getProvider(domainObject.identifier);
|
||||
let result;
|
||||
let lastPersistedTime;
|
||||
@@ -406,7 +395,6 @@ export default class ObjectAPI {
|
||||
lastPersistedTime = domainObject.persisted;
|
||||
const persistedTime = Date.now();
|
||||
this.#mutate(domainObject, 'persisted', persistedTime);
|
||||
|
||||
savedObjectPromise = provider.update(domainObject);
|
||||
}
|
||||
|
||||
@@ -425,11 +413,20 @@ export default class ObjectAPI {
|
||||
}
|
||||
}
|
||||
|
||||
return result.catch((error) => {
|
||||
// suppress conflict error notifications for remotely synced items
|
||||
// (possibly just for notebook and restricted-notebook as they have conflic resolution)
|
||||
if (error instanceof this.errors.Conflict && !this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
|
||||
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
|
||||
return result.catch(async (error) => {
|
||||
if (error instanceof this.errors.Conflict) {
|
||||
// Synchronized objects will resolve their own conflicts
|
||||
if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
|
||||
this.openmct.notifications.info(`Conflict detected while saving "${this.makeKeyString(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;
|
||||
@@ -576,7 +573,6 @@ export default class ObjectAPI {
|
||||
this.#mutate(domainObject, path, value);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
console.log('objectAPI: mutate', JSON.parse(JSON.stringify(domainObject)), path, value);
|
||||
this.transaction.add(domainObject);
|
||||
} else {
|
||||
this.save(domainObject);
|
||||
@@ -608,7 +604,6 @@ export default class ObjectAPI {
|
||||
let unobserve = provider.observe(identifier, (updatedModel) => {
|
||||
// modified can sometimes be undefined, so make it 0 in this case
|
||||
const mutableObjectModification = mutableObject.modified ?? Number.MIN_SAFE_INTEGER;
|
||||
|
||||
if (updatedModel.persisted > mutableObjectModification) {
|
||||
//Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
|
||||
//in rapid succession and intermediate persistence states are returned by the observe function.
|
||||
|
||||
@@ -41,7 +41,6 @@ export default class Transaction {
|
||||
const save = this.objectAPI.save.bind(this.objectAPI);
|
||||
|
||||
Object.values(this.dirtyObjects).forEach(object => {
|
||||
console.log('transaction: commit, objects', object);
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, save));
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -182,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>
|
||||
@@ -192,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';
|
||||
@@ -289,7 +293,8 @@ export default {
|
||||
pan: undefined,
|
||||
animateZoom: true,
|
||||
imagePanned: false,
|
||||
forceShowThumbnails: false
|
||||
forceShowThumbnails: false,
|
||||
animateThumbScroll: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -393,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];
|
||||
},
|
||||
@@ -542,7 +553,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
imageHistory: {
|
||||
handler(newHistory, _oldHistory) {
|
||||
async handler(newHistory, oldHistory) {
|
||||
const newSize = newHistory.length;
|
||||
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
|
||||
if (this.focusedImageTimestamp !== undefined) {
|
||||
@@ -570,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
|
||||
},
|
||||
@@ -584,7 +598,7 @@ export default {
|
||||
this.getImageNaturalDimensions();
|
||||
},
|
||||
bounds() {
|
||||
this.scrollToFocused();
|
||||
this.scrollHandler();
|
||||
},
|
||||
isFixed(newValue) {
|
||||
const isRealTime = !newValue;
|
||||
@@ -774,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);
|
||||
}
|
||||
|
||||
@@ -848,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);
|
||||
|
||||
@@ -855,7 +876,7 @@ export default {
|
||||
this.previousFocusedImage = null;
|
||||
this.setFocusedImage(this.nextImageIndex);
|
||||
this.autoScroll = true;
|
||||
this.scrollToRight();
|
||||
this.scrollHandler();
|
||||
}
|
||||
},
|
||||
scrollToFocused() {
|
||||
@@ -865,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
|
||||
@@ -1087,7 +1123,7 @@ export default {
|
||||
this.setSizedImageDimensions();
|
||||
this.setImageViewport();
|
||||
this.calculateViewHeight();
|
||||
this.scrollToFocused();
|
||||
this.scrollHandler();
|
||||
},
|
||||
setSizedImageDimensions() {
|
||||
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
|
||||
@@ -1122,9 +1158,7 @@ export default {
|
||||
this.handleThumbWindowResizeEnded();
|
||||
},
|
||||
handleThumbWindowResizeEnded() {
|
||||
if (!this.isPaused) {
|
||||
this.scrollToRight('reset');
|
||||
}
|
||||
this.scrollHandler();
|
||||
|
||||
this.calculateViewHeight();
|
||||
|
||||
@@ -1137,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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
@toggleNav="toggleNav"
|
||||
/>
|
||||
<div class="c-notebook__page-view">
|
||||
|
||||
<div class="c-notebook__page-view__header">
|
||||
<button
|
||||
class="c-notebook__toggle-nav-button c-icon-button c-icon-button--major icon-menu-hamburger"
|
||||
@@ -124,8 +123,8 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedPage && !selectedPage.isLocked"
|
||||
class="c-notebook__drag-area icon-plus"
|
||||
:class="{ 'disabled': activeTransaction }"
|
||||
class="c-notebook__drag-area icon-plus"
|
||||
@click="newEntry()"
|
||||
@dragover="dragOver"
|
||||
@drop.capture="dropCapture"
|
||||
@@ -281,14 +280,8 @@ export default {
|
||||
|
||||
return this.sections[0];
|
||||
},
|
||||
showLockButton() {
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
|
||||
return entries && entries.length > 0 && this.isRestricted && !this.selectedPage.isLocked;
|
||||
},
|
||||
sidebarClasses() {
|
||||
let sidebarClasses = [];
|
||||
|
||||
if (this.showNav) {
|
||||
sidebarClasses.push('is-expanded');
|
||||
}
|
||||
@@ -300,12 +293,14 @@ export default {
|
||||
}
|
||||
|
||||
return sidebarClasses;
|
||||
},
|
||||
showLockButton() {
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
|
||||
return entries && entries.length > 0 && this.isRestricted && !this.selectedPage.isLocked;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeTransaction() {
|
||||
console.log('Active Transaction changed', this.activeTransaction);
|
||||
},
|
||||
search() {
|
||||
this.getSearchResults();
|
||||
},
|
||||
@@ -326,10 +321,12 @@ export default {
|
||||
this.formatSidebar();
|
||||
this.setSectionAndPageFromUrl();
|
||||
|
||||
this.transaction = null;
|
||||
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.filterAndSortEntries();
|
||||
this.startObservingEntries();
|
||||
this.unobserveEntries = this.openmct.objects.observe(this.domainObject, '*', this.filterAndSortEntries);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
@@ -356,12 +353,6 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
startObservingEntries() {
|
||||
this.unobserveEntries = this.openmct.objects.observe(this.domainObject, '*', this.filterAndSortEntries);
|
||||
},
|
||||
stopObservingEntries() {
|
||||
this.unobserveEntries();
|
||||
},
|
||||
changeSectionPage(newParams, oldParams, changedParams) {
|
||||
if (isNotebookViewType(newParams.view)) {
|
||||
return;
|
||||
@@ -531,10 +522,10 @@ export default {
|
||||
{
|
||||
label: "Ok",
|
||||
emphasis: true,
|
||||
callback: async () => {
|
||||
callback: () => {
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
entries.splice(entryPos, 1);
|
||||
await this.updateEntries(entries);
|
||||
this.updateEntries(entries);
|
||||
this.filterAndSortEntries();
|
||||
this.removeAnnotations(entryId);
|
||||
dialog.dismiss();
|
||||
@@ -789,7 +780,6 @@ export default {
|
||||
const notebookStorage = this.createNotebookStorageObject();
|
||||
this.updateDefaultNotebook(notebookStorage);
|
||||
const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
|
||||
console.log('newEntry, id, entries', id, JSON.parse(JSON.stringify(this.domainObject.configuration.entries)));
|
||||
this.focusEntryId = id;
|
||||
this.filterAndSortEntries();
|
||||
},
|
||||
@@ -872,23 +862,21 @@ export default {
|
||||
|
||||
setDefaultNotebookSectionId(defaultNotebookSectionId);
|
||||
},
|
||||
async updateEntry(entry) {
|
||||
console.log('update entry', entry);
|
||||
updateEntry(entry) {
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
|
||||
console.log('entry pos', entry, entryPos);
|
||||
entries[entryPos] = entry;
|
||||
|
||||
await this.updateEntries(entries);
|
||||
this.updateEntries(entries);
|
||||
},
|
||||
async updateEntries(entries) {
|
||||
updateEntries(entries) {
|
||||
const configuration = this.domainObject.configuration;
|
||||
const notebookEntries = configuration.entries || {};
|
||||
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
|
||||
|
||||
mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries);
|
||||
|
||||
await this.saveTransaction();
|
||||
this.saveTransaction();
|
||||
},
|
||||
getPageIdFromUrl() {
|
||||
return this.openmct.router.getParams().pageId;
|
||||
@@ -929,45 +917,35 @@ export default {
|
||||
this.filterAndSortEntries();
|
||||
},
|
||||
startTransaction() {
|
||||
console.log('notebook: startTransaction');
|
||||
if (!this.openmct.objects.isTransactionActive()) {
|
||||
this.stopObservingEntries();
|
||||
this.activeTransaction = true;
|
||||
console.log('notebook: startTransaction - starting a new transaction');
|
||||
this.transaction = this.openmct.objects.startTransaction();
|
||||
}
|
||||
},
|
||||
async saveTransaction() {
|
||||
console.log('notebook: saveTransaction');
|
||||
if (this.activeTransaction) {
|
||||
if (this.transaction !== null) {
|
||||
this.savingTransaction = true;
|
||||
console.log('notebook: saveTransaction - saving a transaction');
|
||||
|
||||
try {
|
||||
await this.transaction.commit();
|
||||
} catch (error) {
|
||||
console.log('error committing', error);
|
||||
} finally {
|
||||
this.endTransaction();
|
||||
}
|
||||
|
||||
console.log('notebook: saveTransaction - done saving');
|
||||
this.endTransaction();
|
||||
}
|
||||
},
|
||||
async cancelTransaction() {
|
||||
console.log('notebook: cancelTransaction');
|
||||
if (this.activeTransaction) {
|
||||
this.savingTransaction = true;
|
||||
console.log('notebook: cancelTransaction - canceling a transaction');
|
||||
await this.transaction.cancel();
|
||||
this.endTransaction();
|
||||
if (this.transaction !== null) {
|
||||
try {
|
||||
await this.transaction.cancel();
|
||||
} finally {
|
||||
this.endTransaction();
|
||||
}
|
||||
}
|
||||
},
|
||||
endTransaction() {
|
||||
this.openmct.objects.endTransaction();
|
||||
this.activeTransaction = false;
|
||||
this.transaction = undefined;
|
||||
this.transaction = null;
|
||||
this.savingTransaction = false;
|
||||
this.startObservingEntries();
|
||||
this.activeTransaction = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -283,7 +283,7 @@ export default {
|
||||
await this.addNewEmbed(objectPath);
|
||||
}
|
||||
|
||||
await this.timestampAndUpdate();
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
findPositionInArray(array, id) {
|
||||
let position = -1;
|
||||
@@ -316,14 +316,14 @@ export default {
|
||||
pageId: null
|
||||
});
|
||||
},
|
||||
async removeEmbed(id) {
|
||||
removeEmbed(id) {
|
||||
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
|
||||
// TODO: remove notebook snapshot object using object remove API
|
||||
this.entry.embeds.splice(embedPosition, 1);
|
||||
|
||||
await this.timestampAndUpdate();
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
async updateEmbed(newEmbed) {
|
||||
updateEmbed(newEmbed) {
|
||||
this.entry.embeds.some(e => {
|
||||
const found = (e.id === newEmbed.id);
|
||||
if (found) {
|
||||
@@ -333,7 +333,7 @@ export default {
|
||||
return found;
|
||||
});
|
||||
|
||||
await this.timestampAndUpdate();
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
async timestampAndUpdate() {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
@@ -347,17 +347,14 @@ export default {
|
||||
this.$emit('updateEntry', this.entry);
|
||||
},
|
||||
editingEntry() {
|
||||
console.log('nb entry: editingEntry');
|
||||
this.$emit('editingEntry');
|
||||
},
|
||||
async updateEntryValue($event) {
|
||||
updateEntryValue($event) {
|
||||
const value = $event.target.innerText;
|
||||
if (value !== this.entry.text && value.match(/\S/)) {
|
||||
console.log('nb entry: updateEntryValue - prev val is empty, new val', this.entry.text === '', value);
|
||||
this.entry.text = value;
|
||||
await this.timestampAndUpdate();
|
||||
this.timestampAndUpdate();
|
||||
} else {
|
||||
console.log('nb entry: updateEntryValue, same value not updating');
|
||||
this.$emit('cancelEdit');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,52 +4,43 @@ import _ from 'lodash';
|
||||
export default function (openmct) {
|
||||
const apiSave = openmct.objects.save.bind(openmct.objects);
|
||||
|
||||
openmct.objects.save = async (saveObject) => {
|
||||
let domainObject = cloneObject(saveObject);
|
||||
if (!isNotebookOrAnnotationType(saveObject)) {
|
||||
return apiSave(saveObject);
|
||||
openmct.objects.save = async (domainObject) => {
|
||||
if (!isNotebookOrAnnotationType(domainObject)) {
|
||||
return apiSave(domainObject);
|
||||
}
|
||||
|
||||
// const isNewMutable = !domainObject.isMutable;
|
||||
// const localMutable = openmct.objects.toMutable(domainObject);
|
||||
const isNewMutable = !domainObject.isMutable;
|
||||
const localMutable = openmct.objects.toMutable(domainObject);
|
||||
let result;
|
||||
|
||||
try {
|
||||
console.log('monkeypatch save');
|
||||
result = await apiSave(saveObject);
|
||||
// result = await apiSave(localMutable);
|
||||
result = await apiSave(localMutable);
|
||||
} catch (error) {
|
||||
console.log('monkeypatch save error', error);
|
||||
if (error instanceof openmct.objects.errors.Conflict) {
|
||||
console.log('we got ourselves a conflict');
|
||||
result = await resolveConflicts(domainObject, openmct);
|
||||
// result = await resolveConflicts(domainObject, localMutable, openmct);
|
||||
result = await resolveConflicts(domainObject, localMutable, openmct);
|
||||
} else {
|
||||
result = Promise.reject(error);
|
||||
}
|
||||
} finally {
|
||||
// if (isNewMutable) {
|
||||
// openmct.objects.destroyMutable(localMutable);
|
||||
// }
|
||||
if (isNewMutable) {
|
||||
openmct.objects.destroyMutable(localMutable);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// function resolveConflicts(domainObject, localMutable, openmct) {
|
||||
function resolveConflicts(domainObject, openmct) {
|
||||
function resolveConflicts(domainObject, localMutable, openmct) {
|
||||
if (isNotebookType(domainObject)) {
|
||||
return resolveNotebookEntryConflicts(domainObject, openmct);
|
||||
// return resolveNotebookEntryConflicts(localMutable, openmct);
|
||||
return resolveNotebookEntryConflicts(localMutable, openmct);
|
||||
} else if (isAnnotationType(domainObject)) {
|
||||
return resolveNotebookTagConflicts(domainObject, openmct);
|
||||
// return resolveNotebookTagConflicts(localMutable, openmct);
|
||||
return resolveNotebookTagConflicts(localMutable, openmct);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveNotebookTagConflicts(localAnnotation, openmct) {
|
||||
const localClonedAnnotation = cloneObject(localAnnotation);
|
||||
const localClonedAnnotation = structuredClone(localAnnotation);
|
||||
const remoteMutable = await openmct.objects.getMutable(localClonedAnnotation.identifier);
|
||||
|
||||
// should only be one annotation per targetID, entryID, and tag; so for sanity, ensure we have the
|
||||
@@ -81,40 +72,35 @@ async function resolveNotebookTagConflicts(localAnnotation, openmct) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// async function resolveNotebookEntryConflicts(localMutable, openmct) {
|
||||
async function resolveNotebookEntryConflicts(domainObject, openmct) {
|
||||
if (domainObject.configuration.entries) {
|
||||
// if (localMutable.configuration.entries) {
|
||||
// const localEntries = structuredClone(localMutable.configuration.entries);
|
||||
// const remoteObject = await openmct.objects.remoteGet(localMutable.identifier);
|
||||
const localEntries = domainObject.configuration.entries;
|
||||
const remoteObject = await openmct.objects.remoteGet(domainObject.identifier);
|
||||
async function resolveNotebookEntryConflicts(localMutable, openmct) {
|
||||
if (localMutable.configuration.entries) {
|
||||
const FORCE_REMOTE = true;
|
||||
const localEntries = structuredClone(localMutable.configuration.entries);
|
||||
const remoteObject = await openmct.objects.get(localMutable.identifier, undefined, FORCE_REMOTE);
|
||||
|
||||
return applyLocalEntries(remoteObject, localEntries, openmct);
|
||||
// openmct.objects.destroyMutable(remoteMutable);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyLocalEntries(remoteObject, entries, openmct) {
|
||||
console.log('apply local entries', entries, 'and remote entries', remoteObject.configuration.entries);
|
||||
let shouldSave = false;
|
||||
|
||||
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
|
||||
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
|
||||
const remoteEntries = remoteObject.configuration.entries[sectionKey][pageKey];
|
||||
const mergedEntries = [].concat(remoteEntries);
|
||||
let shouldSave = false;
|
||||
let shouldMutate = false;
|
||||
|
||||
const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id');
|
||||
const locallyModifiedEntries = _.differenceWith(localEntries, remoteEntries, (localEntry, remoteEntry) => {
|
||||
return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text;
|
||||
});
|
||||
|
||||
console.log('locally added', locallyAddedEntries);
|
||||
console.log('locally modified', locallyModifiedEntries);
|
||||
locallyAddedEntries.forEach((localEntry) => {
|
||||
mergedEntries.push(localEntry);
|
||||
shouldSave = true;
|
||||
shouldMutate = true;
|
||||
});
|
||||
|
||||
locallyModifiedEntries.forEach((locallyModifiedEntry) => {
|
||||
@@ -122,25 +108,18 @@ function applyLocalEntries(remoteObject, entries, openmct) {
|
||||
if (mergedEntry !== undefined
|
||||
&& locallyModifiedEntry.text.match(/\S/)) {
|
||||
mergedEntry.text = locallyModifiedEntry.text;
|
||||
shouldSave = true;
|
||||
shouldMutate = true;
|
||||
}
|
||||
});
|
||||
console.log('mergedEntries', mergedEntries);
|
||||
|
||||
if (shouldSave) {
|
||||
remoteObject.configuration.entries = mergedEntries;
|
||||
console.log('save this one', remoteObject);
|
||||
return openmct.objects.save(remoteObject);
|
||||
// openmct.objects.save(remoteObject, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
|
||||
if (shouldMutate) {
|
||||
shouldSave = true;
|
||||
openmct.objects.mutate(remoteObject, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cloneObject(object) {
|
||||
if (typeof window.structuredClone === 'function') {
|
||||
return structuredClone(object);
|
||||
} else {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
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,11 +96,15 @@ 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)) {
|
||||
observer(updatedObject);
|
||||
}
|
||||
@@ -185,7 +189,6 @@ class CouchObjectProvider {
|
||||
}
|
||||
|
||||
async request(subPath, method, body, signal) {
|
||||
console.log('couchobjectprovider.js: request', subPath, method, body, signal);
|
||||
let fetchOptions = {
|
||||
method,
|
||||
body,
|
||||
@@ -221,11 +224,11 @@ class CouchObjectProvider {
|
||||
console.error(error.message);
|
||||
throw new Error(`CouchDB Error - No response"`);
|
||||
} else {
|
||||
if (!isNotebookOrAnnotationType(body.model)) {
|
||||
console.error(error.message);
|
||||
} else {
|
||||
if (body?.model && isNotebookOrAnnotationType(body.model)) {
|
||||
// warn since we handle conflicts for notebooks
|
||||
console.warn(error.message);
|
||||
} else {
|
||||
console.error(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
@@ -241,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 ${JSON.parse(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}`);
|
||||
@@ -260,7 +264,7 @@ class CouchObjectProvider {
|
||||
*/
|
||||
#checkResponse(response, intermediateResponse, key) {
|
||||
let requestSuccess = false;
|
||||
const id = response?.id;
|
||||
const id = response ? response.id : undefined;
|
||||
let rev;
|
||||
|
||||
if (response && response.ok) {
|
||||
@@ -298,13 +302,6 @@ class CouchObjectProvider {
|
||||
}
|
||||
|
||||
if (isNotebookOrAnnotationType(object)) {
|
||||
// check if the object is currently being edited, if so, don't update revision so a conflict will be thrown
|
||||
// and handled with our notebook conflict resolution
|
||||
if (this.openmct.objects.isTransactionActive()
|
||||
&& this.openmct.objects.transaction.getDirtyObject(object.identifier)) {
|
||||
return object;
|
||||
}
|
||||
|
||||
//Temporary measure until object sync is supported for all object types
|
||||
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
|
||||
this.objectQueue[key].updateRevision(response[REV]);
|
||||
@@ -698,7 +695,6 @@ class CouchObjectProvider {
|
||||
}
|
||||
|
||||
update(model) {
|
||||
console.log('couchobjectprovider.js: update', model);
|
||||
let intermediateResponse = this.#getIntermediateResponse();
|
||||
const key = model.identifier.key;
|
||||
model = this.toPersistableModel(model);
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user