Compare commits

..

13 Commits

172 changed files with 713 additions and 1571 deletions

View File

@@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.29.0-focal
- image: mcr.microsoft.com/playwright:v1.25.2-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

View File

@@ -23,7 +23,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.29.0 install
- run: npx playwright@1.25.2 install
- run: npm install
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
- run: npm run test:e2e:couchdb

View File

@@ -30,7 +30,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.29.0 install
- run: npx playwright@1.25.2 install
- run: npx playwright install chrome-beta
- run: npm install
- run: npm run test:e2e:full

View File

@@ -1,175 +0,0 @@
/* global __dirname module */
/*
This is the OpenMCT common webpack file. It is imported by the other three webpack configurations:
- webpack.prod.js - the production configuration for OpenMCT (default)
- webpack.dev.js - the development configuration for OpenMCT
- webpack.coverage.js - imports webpack.dev.js and adds code coverage
There are separate npm scripts to use these configurations, though simply running `npm install`
will use the default production configuration.
*/
const path = require("path");
const packageDefinition = require("../package.json");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { VueLoaderPlugin } = require("vue-loader");
let gitRevision = "error-retrieving-revision";
let gitBranch = "error-retrieving-branch";
try {
gitRevision = require("child_process")
.execSync("git rev-parse HEAD")
.toString()
.trim();
gitBranch = require("child_process")
.execSync("git rev-parse --abbrev-ref HEAD")
.toString()
.trim();
} catch (err) {
console.warn(err);
}
const projectRootDir = path.resolve(__dirname, "..");
/** @type {import('webpack').Configuration} */
const config = {
context: projectRootDir,
entry: {
openmct: "./openmct.js",
generatorWorker: "./example/generator/generatorWorker.js",
couchDBChangesFeed:
"./src/plugins/persistence/couch/CouchChangesFeed.js",
inMemorySearchWorker: "./src/api/objects/InMemorySearchWorker.js",
espressoTheme: "./src/plugins/themes/espresso-theme.scss",
snowTheme: "./src/plugins/themes/snow-theme.scss"
},
output: {
globalObject: "this",
filename: "[name].js",
path: path.resolve(projectRootDir, "dist"),
library: "openmct",
libraryTarget: "umd",
publicPath: "",
hashFunction: "xxhash64",
clean: true
},
resolve: {
alias: {
"@": path.join(projectRootDir, "src"),
legacyRegistry: path.join(projectRootDir, "src/legacyRegistry"),
saveAs: "file-saver/src/FileSaver.js",
csv: "comma-separated-values",
EventEmitter: "eventemitter3",
bourbon: "bourbon.scss",
"plotly-basic": "plotly.js-basic-dist",
"plotly-gl2d": "plotly.js-gl2d-dist",
"d3-scale": path.join(
projectRootDir,
"node_modules/d3-scale/dist/d3-scale.min.js"
),
printj: path.join(
projectRootDir,
"node_modules/printj/dist/printj.min.js"
),
styles: path.join(projectRootDir, "src/styles"),
MCT: path.join(projectRootDir, "src/MCT"),
testUtils: path.join(projectRootDir, "src/utils/testUtils.js"),
objectUtils: path.join(
projectRootDir,
"src/api/objects/object-utils.js"
),
utils: path.join(projectRootDir, "src/utils")
}
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`
}),
new VueLoaderPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: "src/images/favicons",
to: "favicons"
},
{
from: "./index.html",
transform: function (content) {
return content.toString().replace(/dist\//g, "");
}
},
{
from: "src/plugins/imagery/layers",
to: "imagery"
}
]
}),
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[name].css"
})
],
module: {
rules: [
{
test: /\.(sc|sa|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader"
},
{
loader: "resolve-url-loader"
},
{
loader: "sass-loader",
options: { sourceMap: true }
}
]
},
{
test: /\.vue$/,
use: "vue-loader"
},
{
test: /\.html$/,
type: "asset/source"
},
{
test: /\.(jpg|jpeg|png|svg)$/,
type: "asset/resource",
generator: {
filename: "images/[name][ext]"
}
},
{
test: /\.ico$/,
type: "asset/resource",
generator: {
filename: "icons/[name][ext]"
}
},
{
test: /\.(woff|woff2?|eot|ttf)$/,
type: "asset/resource",
generator: {
filename: "fonts/[name][ext]"
}
}
]
},
stats: "errors-warnings",
performance: {
// We should eventually consider chunking to decrease
// these values
maxEntrypointSize: 25000000,
maxAssetSize: 25000000
}
};
module.exports = config;

View File

@@ -1,27 +0,0 @@
/* global __dirname module */
/*
This configuration should be used for production installs.
It is the default webpack configuration.
*/
const path = require("path");
const webpack = require("webpack");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
const projectRootDir = path.resolve(__dirname, "..");
module.exports = merge(common, {
mode: "production",
resolve: {
alias: {
vue: path.join(projectRootDir, "node_modules/vue/dist/vue.min.js")
}
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_ROOT_RELATIVE__: '""'
})
],
devtool: "source-map"
});

View File

@@ -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](#test-architecture-and-ci)
3. [Architecture](#architecture)
## Getting Started
@@ -276,36 +276,14 @@ 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 (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 test (TODO)
#### 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.
@@ -400,23 +378,3 @@ 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`

View File

@@ -45,14 +45,6 @@
* @property {string} url the relative url to the object (for use with `page.goto()`)
*/
/**
* Defines parameters to be used in the creation of a notification.
* @typedef {Object} CreateNotificationOptions
* @property {string} message the message
* @property {'info' | 'alert' | 'error'} severity the severity
* @property {import('../src/api/notifications/NotificationAPI').NotificationOptions} [notificationOptions] additional options
*/
const Buffer = require('buffer').Buffer;
const genUuid = require('uuid').v4;
@@ -120,25 +112,6 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
};
}
/**
* Generate a notification with the given options.
* @param {import('@playwright/test').Page} page
* @param {CreateNotificationOptions} createNotificationOptions
*/
async function createNotification(page, createNotificationOptions) {
await page.evaluate((_createNotificationOptions) => {
const { message, severity, options } = _createNotificationOptions;
const notificationApi = window.openmct.notifications;
if (severity === 'info') {
notificationApi.info(message, options);
} else if (severity === 'alert') {
notificationApi.alert(message, options);
} else {
notificationApi.error(message, options);
}
}, createNotificationOptions);
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} name
@@ -360,7 +333,6 @@ async function setEndOffset(page, offset) {
// eslint-disable-next-line no-undef
module.exports = {
createDomainObjectWithDefaults,
createNotification,
expandTreePaneItemByName,
createPlanFromJSON,
openObjectTreeContextMenu,

View File

@@ -1,76 +0,0 @@
class DomainObjectViewProvider {
constructor(openmct) {
this.key = 'doViewProvider';
this.name = 'Domain Object View Provider';
this.openmct = openmct;
}
canView(domainObject) {
return domainObject.type === 'imageFileInput'
|| domainObject.type === 'jsonFileInput';
}
view(domainObject, objectPath) {
let content;
return {
show: function (element) {
const body = domainObject.selectFile.body;
const type = typeof body;
content = document.createElement('div');
content.id = 'file-input-type';
content.textContent = JSON.stringify(type);
element.appendChild(content);
},
destroy: function (element) {
element.removeChild(content);
content = undefined;
}
};
}
}
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.types.addType('jsonFileInput', {
key: 'jsonFileInput',
name: "JSON File Input Object",
creatable: true,
form: [
{
name: 'Upload File',
key: 'selectFile',
control: 'file-input',
required: true,
text: 'Select File...',
type: 'application/json',
property: [
"selectFile"
]
}
]
});
openmct.types.addType('imageFileInput', {
key: 'imageFileInput',
name: "Image File Input Object",
creatable: true,
form: [
{
name: 'Upload File',
key: 'selectFile',
control: 'file-input',
required: true,
text: 'Select File...',
type: 'image/*',
property: [
"selectFile"
]
}
]
});
openmct.objectViews.addProvider(new DomainObjectViewProvider(openmct));
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
const { test, expect } = require('../../pluginFixtures.js');
const { createDomainObjectWithDefaults, createNotification } = require('../../appActions.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js');
test.describe('AppActions', () => {
test('createDomainObjectsWithDefaults', async ({ page }) => {
@@ -85,28 +85,4 @@ test.describe('AppActions', () => {
expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);
});
});
test("createNotification", async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await createNotification(page, {
message: 'Test info notification',
severity: 'info'
});
await expect(page.locator('.c-message-banner__message')).toHaveText('Test info notification');
await expect(page.locator('.c-message-banner')).toHaveClass(/info/);
await page.locator('[aria-label="Dismiss"]').click();
await createNotification(page, {
message: 'Test alert notification',
severity: 'alert'
});
await expect(page.locator('.c-message-banner__message')).toHaveText('Test alert notification');
await expect(page.locator('.c-message-banner')).toHaveClass(/alert/);
await page.locator('[aria-label="Dismiss"]').click();
await createNotification(page, {
message: 'Test error notification',
severity: 'error'
});
await expect(page.locator('.c-message-banner__message')).toHaveText('Test error notification');
await expect(page.locator('.c-message-banner')).toHaveClass(/error/);
await page.locator('[aria-label="Dismiss"]').click();
});
});

View File

@@ -27,7 +27,7 @@
const { test, expect } = require('../../pluginFixtures');
test.describe("CouchDB Status Indicator with mocked responses @couchdb", () => {
test.describe("CouchDB Status Indicator @couchdb", () => {
test.use({ failOnConsoleError: false });
//TODO BeforeAll Verify CouchDB Connectivity with APIContext
test('Shows green if connected', async ({ page }) => {
@@ -71,41 +71,38 @@ test.describe("CouchDB Status Indicator with mocked responses @couchdb", () => {
});
});
test.describe("CouchDB initialization with mocked responses @couchdb", () => {
test.describe("CouchDB initialization @couchdb", () => {
test.use({ failOnConsoleError: false });
test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
const mockedMissingObjectResponsefromCouchDB = {
status: 404,
contentType: 'application/json',
body: JSON.stringify({})
};
// 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);
}
});
// 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);
// 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({})
});
}, { times: 1 });
// 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.
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Wait for both requests to resolve.
await Promise.all([
putMineFolderRequest,
getMineFolderRequest
]);
// 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);
});
});

View File

@@ -24,14 +24,11 @@
This test suite is dedicated to tests which verify form functionality in isolation
*/
const { test, expect } = require('../../pluginFixtures');
const { test, expect } = require('../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const genUuid = require('uuid').v4;
const path = require('path');
const TEST_FOLDER = 'test folder';
const jsonFilePath = 'e2e/test-data/ExampleLayouts.json';
const imageFilePath = 'e2e/test-data/rick.jpg';
test.describe('Form Validation Behavior', () => {
test('Required Field indicators appear if title is empty and can be corrected', async ({ page }) => {
@@ -70,41 +67,6 @@ test.describe('Form Validation Behavior', () => {
});
});
test.describe('Form File Input Behavior', () => {
test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addInitFileInputObject.js') });
});
test('Can select a JSON file type', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.getByRole('button', { name: ' Create ' }).click();
await page.getByRole('menuitem', { name: 'JSON File Input Object' }).click();
await page.setInputFiles('#fileElem', jsonFilePath);
await page.getByRole('button', { name: 'Save' }).click();
const type = await page.locator('#file-input-type').textContent();
await expect(type).toBe(`"string"`);
});
test('Can select an image file type', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.getByRole('button', { name: ' Create ' }).click();
await page.getByRole('menuitem', { name: 'Image File Input Object' }).click();
await page.setInputFiles('#fileElem', imageFilePath);
await page.getByRole('button', { name: 'Save' }).click();
const type = await page.locator('#file-input-type').textContent();
await expect(type).toBe(`"object"`);
});
});
test.describe('Persistence operations @addInit', () => {
// add non persistable root item
test.beforeEach(async ({ page }) => {
@@ -166,108 +128,6 @@ 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', () => {

View File

@@ -1,39 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify Open MCT's Notification functionality
*/
// FIXME: Remove this eslint exception once tests are implemented
// eslint-disable-next-line no-unused-vars
const { test, expect } = require('../../pluginFixtures');
test.describe('Notifications List', () => {
test.fixme('Notifications can be dismissed individually', async ({ page }) => {
// Create some persistent notifications
// Verify that they are present in the notifications list
// Dismiss one of the notifications
// Verify that it is no longer present in the notifications list
// Verify that the other notifications are still present in the notifications list
});
});

View File

@@ -44,7 +44,6 @@ 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;

View File

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

View File

@@ -1,58 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* This test is dedicated to test notification banner functionality and its accessibility attributes.
*/
const { test, expect } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
test.describe('Visual - Check Notification Info Banner of \'Save successful\'', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL and Hide Tree
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Create a clock, click on \'Save successful\' banner and dismiss it', async ({ page }) => {
// Create a clock domain object
await createDomainObjectWithDefaults(page, { type: 'Clock' });
// Verify there is a button with aria-label="Review 1 Notification"
expect(await page.locator('button[aria-label="Review 1 Notification"]').isVisible()).toBe(true);
// Verify there is a button with aria-label="Clear all notifications"
expect(await page.locator('button[aria-label="Clear all notifications"]').isVisible()).toBe(true);
// Click on the div with role="alert" that has "Save successful" text
await page.locator('div[role="alert"]:has-text("Save successful")').click();
// Verify there is a div with role="dialog"
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true);
// Verify the div with role="dialog" contains text "Save successful"
expect(await page.locator('div[role="dialog"]').innerText()).toContain('Save successful');
await percySnapshot(page, 'Notification banner');
// Verify there is a button with text "Dismiss"
expect(await page.locator('button:has-text("Dismiss")').isVisible()).toBe(true);
// Click on button with text "Dismiss"
await page.locator('button:has-text("Dismiss")').click();
// Verify there is no div with role="dialog"
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
});
});

View File

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

View File

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

View File

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

View File

@@ -64,6 +64,7 @@
</style>
</head>
<body>
<div></div>
</body>
<script>
const THIRTY_SECONDS = 30 * 1000;
@@ -75,7 +76,8 @@
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
openmct.install(openmct.plugins.LocalStorage());
// openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin());

View File

@@ -28,12 +28,12 @@ module.exports = (config) => {
let singleRun;
if (process.env.KARMA_DEBUG) {
webpackConfig = require("./.webpack/webpack.dev.js");
browsers = ["ChromeDebugging"];
webpackConfig = require('./webpack.dev.js');
browsers = ['ChromeDebugging'];
singleRun = false;
} else {
webpackConfig = require("./.webpack/webpack.coverage.js");
browsers = ["ChromeHeadless"];
webpackConfig = require('./webpack.coverage.js');
browsers = ['ChromeHeadless'];
singleRun = true;
}
@@ -42,28 +42,28 @@ module.exports = (config) => {
delete webpackConfig.entry;
config.set({
basePath: "",
frameworks: ["jasmine", "webpack"],
basePath: '',
frameworks: ['jasmine', 'webpack'],
files: [
"indexTest.js",
'indexTest.js',
// included means: should the files be included in the browser using <script> tag?
// We don't want them as a <script> because the shared worker source
// needs loaded remotely by the shared worker process.
{
pattern: "dist/couchDBChangesFeed.js*",
pattern: 'dist/couchDBChangesFeed.js*',
included: false
},
{
pattern: "dist/inMemorySearchWorker.js*",
pattern: 'dist/inMemorySearchWorker.js*',
included: false
},
{
pattern: "dist/generatorWorker.js*",
pattern: 'dist/generatorWorker.js*',
included: false
}
],
port: 9876,
reporters: ["spec", "junit", "coverage-istanbul"],
reporters: ['spec', 'junit', 'coverage-istanbul'],
browsers,
client: {
jasmine: {
@@ -73,8 +73,8 @@ module.exports = (config) => {
},
customLaunchers: {
ChromeDebugging: {
base: "Chrome",
flags: ["--remote-debugging-port=9222"],
base: 'Chrome',
flags: ['--remote-debugging-port=9222'],
debug: true
}
},
@@ -90,7 +90,7 @@ module.exports = (config) => {
fixWebpackSourcePaths: true,
skipFilesWithNoCoverage: true,
dir: "coverage/unit", //Sets coverage file to be consumed by codecov.io
reports: ["lcovonly"]
reports: ['lcovonly']
},
specReporter: {
maxLogLines: 5,
@@ -102,11 +102,11 @@ module.exports = (config) => {
failFast: false
},
preprocessors: {
"indexTest.js": ["webpack", "sourcemap"]
'indexTest.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {
stats: "errors-warnings"
stats: 'errors-warnings'
},
concurrency: 1,
singleRun,

View File

@@ -1,29 +1,28 @@
{
"name": "openmct",
"version": "2.1.6-SNAPSHOT",
"version": "2.1.4-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.2",
"@percy/cli": "1.17.0",
"@percy/cli": "1.16.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.29.0",
"@types/eventemitter3": "1.2.0",
"@playwright/test": "1.25.2",
"@types/jasmine": "4.3.1",
"@types/lodash": "4.14.191",
"babel-loader": "9.1.0",
"babel-loader": "9.0.0",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "11.0.0",
"css-loader": "6.7.3",
"css-loader": "6.7.1",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.32.0",
"eslint": "8.29.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.11.2",
"eslint-plugin-vue": "9.9.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",
@@ -39,31 +38,32 @@
"karma-jasmine": "5.1.0",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.36",
"karma-spec-reporter": "0.0.34",
"karma-webpack": "5.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.7.2",
"moment": "2.29.4",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.40",
"moment-timezone": "0.5.38",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.29.0",
"plotly.js-basic-dist": "2.17.0",
"plotly.js-gl2d-dist": "2.17.1",
"playwright-core": "1.25.2",
"plotly.js-basic-dist": "2.14.0",
"plotly.js-gl2d-dist": "2.14.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sass": "1.57.1",
"sass-loader": "13.2.0",
"sinon": "15.0.1",
"sass": "1.56.1",
"sass-loader": "13.0.2",
"sinon": "14.0.1",
"style-loader": "^3.3.1",
"typescript": "4.9.4",
"typescript": "4.9.3",
"uuid": "9.0.0",
"vue": "2.6.14",
"vue": "^3.1.0",
"@vue/compat": "^3.1.0",
"vue-eslint-parser": "9.1.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"vue-loader": "^16.0.0",
"@vue/compiler-sfc": "^3.1.0",
"webpack": "5.74.0",
"webpack-cli": "5.0.0",
"webpack-dev-server": "4.11.1",
@@ -72,14 +72,14 @@
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
"start": "npx webpack serve --config ./webpack.dev.js",
"start:coverage": "npx webpack serve --config ./webpack.coverage.js",
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "webpack --config ./.webpack/webpack.prod.js",
"build:dev": "webpack --config ./.webpack/webpack.dev.js",
"build:coverage": "webpack --config ./.webpack/webpack.coverage.js",
"build:watch": "webpack --config ./.webpack/webpack.dev.js --watch",
"build:prod": "webpack --config webpack.prod.js",
"build:dev": "webpack --config webpack.dev.js",
"build:coverage": "webpack --config webpack.coverage.js",
"build:watch": "webpack --config webpack.dev.js --watch",
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
"test": "karma start",
"test:debug": "KARMA_DEBUG=true karma start",

View File

@@ -328,7 +328,7 @@ define([
* @param {HTMLElement} [domElement] the DOM element in which to run
* MCT; if undefined, MCT will be run in the body of the document
*/
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
MCT.prototype.start = function (domElement = document.body.firstElementChild, isHeadlessMode = false) {
if (this.types.get('layout') === undefined) {
this.install(this.plugins.DisplayLayout({
showAsView: ['summary-widget']
@@ -349,7 +349,7 @@ define([
*/
if (!isHeadlessMode) {
const appLayout = new Vue({
const appLayout = Vue.createApp({
components: {
'Layout': Layout.default
},
@@ -358,9 +358,8 @@ define([
},
template: '<Layout ref="layout"></Layout>'
});
domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout.$refs.layout;
appLayout.mount(domElement);
this.layout = appLayout._instance.refs.layout;
Browse(this);
}

View File

@@ -73,10 +73,6 @@ 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)

View File

@@ -109,7 +109,7 @@ export default {
this.formControl.show(this.$refs.rowElement, this.row, this.onChange);
},
destroyed() {
unmounted() {
const destroy = this.formControl.destroy;
if (destroy) {
destroy();

View File

@@ -173,7 +173,7 @@ export default {
this.options = this.model.options;
}
},
destroyed() {
unmounted() {
document.body.removeEventListener('click', this.handleOutsideClick);
},
methods: {

View File

@@ -30,7 +30,7 @@
id="fileElem"
ref="fileInput"
type="file"
:accept="acceptableFileTypes"
accept=".json"
style="display:none"
>
<button
@@ -72,13 +72,6 @@ export default {
},
removable() {
return (this.fileInfo || this.model.value) && this.model.removable;
},
acceptableFileTypes() {
if (this.model.type) {
return this.model.type;
}
return 'application/json';
}
},
mounted() {
@@ -87,13 +80,7 @@ export default {
methods: {
handleFiles() {
const fileList = this.$refs.fileInput.files;
const file = fileList[0];
if (this.acceptableFileTypes === 'application/json') {
this.readFile(file);
} else {
this.handleRawFile(file);
}
this.readFile(fileList[0]);
},
readFile(file) {
const self = this;
@@ -117,21 +104,6 @@ export default {
fileReader.readAsText(file);
},
handleRawFile(file) {
const fileInfo = {
name: file.name,
body: file
};
this.fileInfo = Object.assign({}, fileInfo);
const data = {
model: this.model,
value: fileInfo
};
this.$emit('onChange', data);
},
selectFile() {
this.$refs.fileInput.click();
},

View File

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

View File

@@ -9,11 +9,9 @@
>
<template
v-for="(actionGroups, index) in options.actions"
:key="index"
>
<div
:key="index"
role="group"
>
<div role="group">
<li
v-for="action in actionGroups"
:key="action.name"

View File

@@ -10,11 +10,9 @@
>
<template
v-for="(actionGroups, index) in options.actions"
:key="index"
>
<div
:key="index"
role="group"
>
<div role="group">
<li
v-for="action in actionGroups"
:key="action.name"

View File

@@ -31,31 +31,7 @@
* @namespace platform/api/notifications
*/
import moment from 'moment';
import EventEmitter from 'eventemitter3';
/**
* @typedef {object} NotificationProperties
* @property {function} dismiss Dismiss the notification
* @property {NotificationModel} model The Notification model
* @property {(progressPerc: number, progressText: string) => void} [progress] Update the progress of the notification
*/
/**
* @typedef {EventEmitter & NotificationProperties} Notification
*/
/**
* @typedef {object} NotificationLink
* @property {function} onClick The function to be called when the link is clicked
* @property {string} cssClass A CSS class name to style the link
* @property {string} text The text to be displayed for the link
*/
/**
* @typedef {object} NotificationOptions
* @property {number} [autoDismissTimeout] Milliseconds to wait before automatically dismissing the notification
* @property {NotificationLink} [link] A link for the notification
*/
import EventEmitter from 'EventEmitter';
/**
* A representation of a banner notification. Banner notifications
@@ -64,17 +40,13 @@ import EventEmitter from 'eventemitter3';
* dialogs so that the same information can be provided in a dialog
* and then minimized to a banner notification if needed, or vice-versa.
*
* @see DialogModel
* @typedef {object} NotificationModel
* @property {string} message The message to be displayed by the notification
* @property {number | 'unknown'} [progress] The progress of some ongoing task. Should be a number between 0 and 100, or
* with the string literal 'unknown'.
* @property {string} [progressText] A message conveying progress of some ongoing task.
* @property {string} [severity] The severity of the notification. Should be one of 'info', 'alert', or 'error'.
* @property {string} [timestamp] The time at which the notification was created. Should be a string in ISO 8601 format.
* @property {boolean} [minimized] Whether or not the notification has been minimized
* @property {boolean} [autoDismiss] Whether the notification should be automatically dismissed after a short period of time.
* @property {NotificationOptions} options The notification options
* @see DialogModel
*/
const DEFAULT_AUTO_DISMISS_TIMEOUT = 3000;
@@ -83,19 +55,18 @@ const MINIMIZE_ANIMATION_TIMEOUT = 300;
/**
* The notification service is responsible for informing the user of
* events via the use of banner notifications.
*/
* @memberof ui/notification
* @constructor */
export default class NotificationAPI extends EventEmitter {
constructor() {
super();
/** @type {Notification[]} */
this.notifications = [];
/** @type {{severity: "info" | "alert" | "error"}} */
this.highest = { severity: "info" };
/**
/*
* A context in which to hold the active notification and a
* handle to its timeout.
* @type {Notification | undefined}
*/
this.activeNotification = undefined;
}
@@ -104,12 +75,16 @@ export default class NotificationAPI extends EventEmitter {
* Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief
* period of time.
* @param {string} message The message to display to the user
* @param {NotificationOptions} [options] The notification options
* @returns {Notification}
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link
* text: text to display for link
* @returns {InfoNotification}
*/
info(message, options = {}) {
/** @type {NotificationModel} */
const notificationModel = {
let notificationModel = {
message: message,
autoDismiss: true,
severity: "info",
@@ -122,7 +97,7 @@ export default class NotificationAPI extends EventEmitter {
/**
* Present an alert to the user.
* @param {string} message The message to display to the user.
* @param {NotificationOptions} [options] object with following properties
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
@@ -131,7 +106,7 @@ export default class NotificationAPI extends EventEmitter {
* @returns {Notification}
*/
alert(message, options = {}) {
const notificationModel = {
let notificationModel = {
message: message,
severity: "alert",
options
@@ -172,8 +147,7 @@ export default class NotificationAPI extends EventEmitter {
message: message,
progressPerc: progressPerc,
progressText: progressText,
severity: "info",
options: {}
severity: "info"
};
return this._notify(notificationModel);
@@ -191,13 +165,8 @@ export default class NotificationAPI extends EventEmitter {
* dismissed.
*
* @private
* @param {Notification | undefined} notification
*/
_minimize(notification) {
if (!notification) {
return;
}
//Check this is a known notification
let index = this.notifications.indexOf(notification);
@@ -235,13 +204,8 @@ export default class NotificationAPI extends EventEmitter {
* dismiss
*
* @private
* @param {Notification | undefined} notification
*/
_dismiss(notification) {
if (!notification) {
return;
}
//Check this is a known notification
let index = this.notifications.indexOf(notification);
@@ -272,11 +236,10 @@ export default class NotificationAPI extends EventEmitter {
* dismiss or minimize where appropriate.
*
* @private
* @param {Notification | undefined} notification
*/
_dismissOrMinimize(notification) {
let model = notification?.model;
if (model?.severity === "info") {
let model = notification.model;
if (model.severity === "info") {
this._dismiss(notification);
} else {
this._minimize(notification);
@@ -288,11 +251,10 @@ export default class NotificationAPI extends EventEmitter {
*/
_setHighestSeverity() {
let severity = {
info: 1,
alert: 2,
error: 3
"info": 1,
"alert": 2,
"error": 3
};
this.highest.severity = this.notifications.reduce((previous, notification) => {
if (severity[notification.model.severity] > severity[previous]) {
return notification.model.severity;
@@ -350,11 +312,8 @@ export default class NotificationAPI extends EventEmitter {
/**
* @private
* @param {NotificationModel} notificationModel
* @returns {Notification}
*/
_createNotification(notificationModel) {
/** @type {Notification} */
let notification = new EventEmitter();
notification.model = notificationModel;
notification.dismiss = () => {
@@ -374,7 +333,6 @@ export default class NotificationAPI extends EventEmitter {
/**
* @private
* @param {Notification | undefined} notification
*/
_setActiveNotification(notification) {
this.activeNotification = notification;

View File

@@ -193,27 +193,23 @@ 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, forceRemote = false) {
get(identifier, abortSignal) {
let keystring = this.makeKeyString(identifier);
if (!forceRemote) {
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
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);
}
}
@@ -395,6 +391,7 @@ export default class ObjectAPI {
lastPersistedTime = domainObject.persisted;
const persistedTime = Date.now();
this.#mutate(domainObject, 'persisted', persistedTime);
savedObjectPromise = provider.update(domainObject);
}
@@ -402,7 +399,7 @@ export default class ObjectAPI {
savedObjectPromise.then(response => {
savedResolve(response);
}).catch((error) => {
if (!isNewObject) {
if (lastPersistedTime !== undefined) {
this.#mutate(domainObject, 'persisted', lastPersistedTime);
}
@@ -413,20 +410,9 @@ export default class ObjectAPI {
}
}
return result.catch(async (error) => {
return result.catch((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);
}
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
}
throw error;

View File

@@ -15,8 +15,6 @@
ref="element"
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0"
aria-modal="true"
role="dialog"
></div>
<div
v-if="buttons"

View File

@@ -147,7 +147,7 @@ export default {
this.setUnit();
}
},
destroyed() {
unmounted() {
this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.resetValues);

View File

@@ -89,7 +89,7 @@ export default {
this.composition.on('reorder', this.reorder);
this.composition.load();
},
destroyed() {
unmounted() {
this.composition.off('add', this.addItem);
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.reorder);

View File

@@ -33,11 +33,9 @@
<tbody>
<template
v-for="ladTable in ladTableObjects"
:key="ladTable.key"
>
<tr
:key="ladTable.key"
class="c-table__group-header js-lad-table-set__table-headers"
>
<tr class="c-table__group-header js-lad-table-set__table-headers">
<td colspan="10">
{{ ladTable.domainObject.name }}
</td>
@@ -104,7 +102,7 @@ export default {
this.composition.on('reorder', this.reorderLadTables);
this.composition.load();
},
destroyed() {
unmounted() {
this.composition.off('add', this.addLadTable);
this.composition.off('remove', this.removeLadTable);
this.composition.off('reorder', this.reorderLadTables);

View File

@@ -93,7 +93,7 @@ define([
rowCount: 'reflow'
},
template: autoflowTemplate,
destroyed: function () {
unmounted: function () {
controller.destroy();
if (interval) {

View File

@@ -84,7 +84,7 @@ export default {
});
this.registerListeners();
},
beforeDestroy() {
beforeUnmount() {
if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer);

View File

@@ -71,7 +71,7 @@ export default {
this.unobserveInterpolation = this.openmct.objects.observe(this.domainObject, 'configuration.useInterpolation', this.refreshData);
this.unobserveBar = this.openmct.objects.observe(this.domainObject, 'configuration.useBar', this.refreshData);
},
beforeDestroy() {
beforeUnmount() {
this.stopFollowingTimeContext();
this.removeAllSubscriptions();

View File

@@ -209,7 +209,7 @@ export default {
this.registerListeners();
this.composition.load();
},
beforeDestroy() {
beforeUnmount() {
this.openmct.editor.off('isEditing', this.setEditState);
this.stopListening();
},

View File

@@ -115,7 +115,7 @@ export default {
this.initColorAndName();
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
},
beforeDestroy() {
beforeUnmount() {
if (this.removeBarStylesListener) {
this.removeBarStylesListener();
}

View File

@@ -69,7 +69,7 @@ export default {
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.reloadTelemetry);
this.unobserveUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.reloadTelemetry);
},
beforeDestroy() {
beforeUnmount() {
this.stopFollowingTimeContext();
if (!this.composition) {

View File

@@ -91,7 +91,7 @@ export default {
this.$refs.plot.on('plotly_relayout', this.zoom);
},
beforeDestroy() {
beforeUnmount() {
if (this.$refs.plot && this.$refs.plot.off) {
this.$refs.plot.off('plotly_relayout', this.zoom);
}

View File

@@ -52,7 +52,7 @@ export default {
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
beforeUnmount() {
this.openmct.editor.off('isEditing', this.setEditState);
},
methods: {

View File

@@ -70,7 +70,7 @@ export default {
this.registerListeners();
this.composition.load();
},
beforeDestroy() {
beforeUnmount() {
this.stopListening();
},
methods: {

View File

@@ -101,7 +101,7 @@ export default {
this.registerListeners();
this.composition.load();
},
beforeDestroy() {
beforeUnmount() {
this.stopListening();
},
methods: {

View File

@@ -85,7 +85,7 @@ export default {
mounted() {
this.unlisten = ticker.listen(this.tick);
},
beforeDestroy() {
beforeUnmount() {
if (this.unlisten) {
this.unlisten();
}

View File

@@ -48,7 +48,7 @@ export default {
mounted() {
this.unlisten = ticker.listen(this.tick);
},
beforeDestroy() {
beforeUnmount() {
if (this.unlisten) {
this.unlisten();
}

View File

@@ -295,7 +295,7 @@ export default {
return false;
}
},
destroyed() {
unmounted() {
this.destroy();
},
mounted() {

View File

@@ -128,7 +128,7 @@ export default {
deep: true
}
},
destroyed() {
unmounted() {
this.composition.off('add', this.addTelemetryObject);
this.composition.off('remove', this.removeTelemetryObject);
if (this.conditionManager) {

View File

@@ -180,7 +180,7 @@ export default {
deep: true
}
},
beforeDestroy() {
beforeUnmount() {
this.resetApplied();
},
mounted() {

View File

@@ -253,7 +253,7 @@ export default {
return this.styleableFontItems.length && this.allowEditing;
}
},
destroyed() {
unmounted() {
this.removeListeners();
this.openmct.editor.off('isEditing', this.setEditState);
this.stylesManager.off('styleSelected', this.applyStyleToSelection);

View File

@@ -75,7 +75,7 @@ export default {
this.listenToConditionSetChanges();
}
},
beforeDestroy() {
beforeUnmount() {
this.conditionSetIdentifier = null;
if (this.unlisten) {

View File

@@ -66,7 +66,7 @@ export default {
this.openmct.selection.on('change', this.handleSelection);
this.handleSelection(this.openmct.selection.get());
},
destroyed() {
unmounted() {
this.openmct.editor.off('isEditing', this.toggleEdit);
this.openmct.selection.off('change', this.handleSelection);
},

View File

@@ -113,7 +113,7 @@ export default {
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.initSelect);
},
destroyed() {
unmounted() {
if (this.removeSelectable) {
this.removeSelectable();
}

View File

@@ -225,7 +225,7 @@ export default {
this.watchDisplayResize();
},
destroyed: function () {
unmounted: function () {
this.openmct.selection.off('change', this.setSelection);
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);

View File

@@ -113,7 +113,7 @@ export default {
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.initSelect);
},
destroyed() {
unmounted() {
if (this.removeSelectable) {
this.removeSelectable();
}

View File

@@ -120,7 +120,7 @@ export default {
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.initSelect);
},
destroyed() {
unmounted() {
if (this.removeSelectable) {
this.removeSelectable();
}

View File

@@ -269,7 +269,7 @@ export default {
};
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context, this.initSelect);
},
destroyed() {
unmounted() {
if (this.removeSelectable) {
this.removeSelectable();
}

View File

@@ -138,7 +138,7 @@ export default {
.then(this.setObject);
}
},
beforeDestroy() {
beforeUnmount() {
if (this.removeSelectable) {
this.removeSelectable();
}

View File

@@ -225,7 +225,7 @@ export default {
this.status = this.openmct.status.get(this.item.identifier);
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
},
beforeDestroy() {
beforeUnmount() {
this.removeStatusListener();
if (this.removeSelectable) {

View File

@@ -122,7 +122,7 @@ export default {
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.initSelect);
},
destroyed() {
unmounted() {
if (this.removeSelectable) {
this.removeSelectable();
}

View File

@@ -38,7 +38,7 @@ export default {
this.objectStyle = this.getObjectStyleForItem(this.parentDomainObject.configuration.objectStyles);
this.initObjectStyles();
},
beforeDestroy() {
beforeUnmount() {
if (this.stopListeningObjectStyles) {
this.stopListeningObjectStyles();
}

View File

@@ -47,7 +47,7 @@ export default {
this.unsubscribe = this.openmct.faults
.subscribe(this.domainObject, this.updateFault);
},
beforeDestroy() {
beforeUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
}

View File

@@ -89,7 +89,7 @@ export default {
mounted() {
this.openmct.editor.on('isEditing', this.toggleIsEditing);
},
beforeDestroy() {
beforeUnmount() {
this.openmct.editor.off('isEditing', this.toggleIsEditing);
},
methods: {

View File

@@ -121,7 +121,7 @@ export default {
this.objectCssClass = type.definition.cssClass;
this.openmct.editor.on('isEditing', this.toggleIsEditing);
},
beforeDestroy() {
beforeUnmount() {
this.openmct.editor.off('isEditing', this.toggleIsEditing);
},
methods: {

View File

@@ -91,7 +91,7 @@ export default {
this.unobserve = this.openmct.objects.observe(this.providedObject, 'configuration.filters', this.updatePersistedFilters);
this.unobserveGlobalFilters = this.openmct.objects.observe(this.providedObject, 'configuration.globalFilters', this.updateGlobalFilters);
},
beforeDestroy() {
beforeUnmount() {
this.composition.off('add', this.addChildren);
this.composition.off('remove', this.removeChildren);
this.unobserve();

View File

@@ -45,9 +45,9 @@
<div class="c-fl-container__frames-holder">
<template
v-for="(frame, i) in frames"
:key="frame.id"
>
<frame-component
:key="frame.id"
class="c-fl-container__frame"
:frame="frame"
:index="i"
@@ -57,7 +57,6 @@
/>
<drop-hint
:key="'hint-' + i"
class="c-fl-frame__drop-hint"
:index="i"
:allow-drop="allowDrop"
@@ -66,7 +65,6 @@
<resize-handle
v-if="(i !== frames.length - 1)"
:key="'handle-' + i"
:index="i"
:orientation="rowsLayout ? 'horizontal' : 'vertical'"
:is-editing="isEditing"
@@ -134,7 +132,7 @@ export default {
this.unsubscribeSelection = this.openmct.selection.selectable(this.$el, context, false);
},
beforeDestroy() {
beforeUnmount() {
this.unsubscribeSelection();
},
methods: {

View File

@@ -56,7 +56,7 @@ export default {
document.addEventListener('dragend', this.dragend);
document.addEventListener('drop', this.dragend);
},
destroyed() {
unmounted() {
document.removeEventListener('dragstart', this.dragstart);
document.removeEventListener('dragend', this.dragend);
document.removeEventListener('drop', this.dragend);

View File

@@ -40,10 +40,12 @@
'c-fl--rows': rowsLayout === true
}"
>
<template v-for="(container, index) in containers">
<template
v-for="(container, index) in containers"
:key="`hint-top-${container.id}`"
>
<drop-hint
v-if="index === 0 && containers.length > 1"
:key="`hint-top-${container.id}`"
class="c-fl-frame__drop-hint"
:index="-1"
:allow-drop="allowContainerDrop"
@@ -51,7 +53,6 @@
/>
<container-component
:key="`component-${container.id}`"
class="c-fl__container"
:index="index"
:container="container"
@@ -66,7 +67,6 @@
<resize-handle
v-if="index !== (containers.length - 1)"
:key="`handle-${container.id}`"
:index="index"
:orientation="rowsLayout ? 'vertical' : 'horizontal'"
:is-editing="isEditing"
@@ -77,7 +77,6 @@
<drop-hint
v-if="containers.length > 1"
:key="`hint-bottom-${container.id}`"
class="c-fl-frame__drop-hint"
:index="index"
:allow-drop="allowContainerDrop"
@@ -180,7 +179,7 @@ export default {
this.composition.on('add', this.addFrame);
this.composition.load();
},
beforeDestroy() {
beforeUnmount() {
this.composition.off('remove', this.removeChildObject);
this.composition.off('add', this.addFrame);
},

View File

@@ -113,7 +113,7 @@ export default {
this.dragGhost = document.getElementById('js-fl-drag-ghost');
},
beforeDestroy() {
beforeUnmount() {
if (this.domainObjectPromise) {
this.domainObjectPromise.then(() => {
if (this?.domainObject?.isMutable) {

View File

@@ -56,7 +56,7 @@ export default {
document.addEventListener('dragend', this.unsetDragging);
document.addEventListener('drop', this.unsetDragging);
},
destroyed() {
unmounted() {
document.removeEventListener('dragstart', this.setDragging);
document.removeEventListener('dragend', this.unsetDragging);
document.removeEventListener('drop', this.unsetDragging);

View File

@@ -23,7 +23,7 @@ export default {
this.composition.on('remove', this.remove);
this.composition.load();
},
destroyed() {
unmounted() {
if (!this.composition) {
return;
}

View File

@@ -27,7 +27,7 @@ export default {
this.status = this.openmct.status.get(identifier);
this.removeStatusListener = this.openmct.status.observe(identifier, this.setStatus);
},
destroyed() {
unmounted() {
this.removeStatusListener();
}
};

View File

@@ -73,21 +73,19 @@ export default class CreateAction extends PropertiesAction {
title: 'Saving'
});
try {
await this.openmct.objects.save(this.domainObject);
const success = await this.openmct.objects.save(this.domainObject);
if (success) {
const compositionCollection = await this.openmct.composition.get(parentDomainObject);
compositionCollection.add(this.domainObject);
this._navigateAndEdit(this.domainObject, parentDomainObjectPath);
this.openmct.notifications.info('Save successful');
} catch (err) {
console.error(err);
this.openmct.notifications.error(`Error saving objects: ${err}`);
} finally {
dialog.dismiss();
} else {
this.openmct.notifications.error('Error saving objects');
}
dialog.dismiss();
}
/**

View File

@@ -536,7 +536,7 @@ export default {
this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('timeSystem', this.setTimeSystem);
},
destroyed() {
unmounted() {
this.composition.off('add', this.addedToComposition);
this.composition.off('remove', this.removeTelemetryObject);

View File

@@ -159,7 +159,7 @@ export default {
document.addEventListener('keyup', this.handleKeyUp);
this.clearWheelZoom = _.debounce(this.clearWheelZoom, 600);
},
beforeDestroy() {
beforeUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
},

View File

@@ -93,7 +93,7 @@ export default {
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
beforeDestroy() {
beforeUnmount() {
if (this.imageryStripResizeObserver) {
this.imageryStripResizeObserver.disconnect();
}

View File

@@ -25,7 +25,7 @@
tabindex="0"
class="c-imagery"
@keyup="arrowUpHandler"
@keydown.prevent="arrowDownHandler"
@keydown="arrowDownHandler"
@mouseover="focusElement"
>
<div
@@ -147,7 +147,7 @@
v-if="!isFixed"
class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}"
@click="handlePauseButton(!isPaused)"
@click="paused(!isPaused)"
></button>
</div>
</div>
@@ -165,9 +165,6 @@
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-scroll-area"
:class="[{
'animate-scroll': animateThumbScroll
}]"
@scroll="handleScroll"
>
<ImageThumbnail
@@ -178,14 +175,14 @@
:selected="focusedImageIndex === index && isPaused"
:real-time="!isFixed"
:viewable-area="focusedImageIndex === index ? viewableArea : null"
@click.native="thumbnailClicked(index)"
@click="thumbnailClicked(index)"
/>
</div>
<button
class="c-imagery__auto-scroll-resume-button c-icon-button icon-play"
title="Resume automatic scrolling of image thumbnails"
@click="scrollToRight"
@click="scrollToRight('reset')"
></button>
</div>
</div>
@@ -195,7 +192,6 @@
import eventHelpers from '../lib/eventHelpers';
import _ from 'lodash';
import moment from 'moment';
import Vue from 'vue';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
@@ -293,8 +289,7 @@ export default {
pan: undefined,
animateZoom: true,
imagePanned: false,
forceShowThumbnails: false,
animateThumbScroll: false
forceShowThumbnails: false
};
},
computed: {
@@ -398,12 +393,6 @@ export default {
return disabled;
},
isComposedInLayout() {
return (
this.currentView?.objectPath
&& !this.openmct.router.isNavigatedObject(this.currentView.objectPath)
);
},
focusedImage() {
return this.imageHistory[this.focusedImageIndex];
},
@@ -553,7 +542,7 @@ export default {
},
watch: {
imageHistory: {
async handler(newHistory, oldHistory) {
handler(newHistory, _oldHistory) {
const newSize = newHistory.length;
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
if (this.focusedImageTimestamp !== undefined) {
@@ -581,13 +570,10 @@ export default {
if (!this.isPaused) {
this.setFocusedImage(imageIndex);
this.scrollToRight();
} else {
this.scrollToFocused();
}
await this.scrollHandler();
if (oldHistory?.length > 0) {
this.animateThumbScroll = true;
}
},
deep: true
},
@@ -598,7 +584,7 @@ export default {
this.getImageNaturalDimensions();
},
bounds() {
this.scrollHandler();
this.scrollToFocused();
},
isFixed(newValue) {
const isRealTime = !newValue;
@@ -657,7 +643,7 @@ export default {
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
this.loadVisibleLayers();
},
beforeDestroy() {
beforeUnmount() {
this.persistVisibleLayers();
this.stopFollowingTimeContext();
@@ -788,7 +774,7 @@ export default {
}
},
persistVisibleLayers() {
if (this.domainObject.configuration && this.openmct.objects.supportsMutation(this.domainObject.identifier)) {
if (this.domainObject.configuration) {
this.openmct.objects.mutate(this.domainObject, 'configuration.layers', this.layers);
}
@@ -862,13 +848,6 @@ export default {
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
this.autoScroll = !disableScroll;
},
handlePauseButton(newState) {
this.paused(newState);
if (newState) {
// need to set the focused index or the paused focus will drift
this.thumbnailClicked(this.focusedImageIndex);
}
},
paused(state) {
this.isPaused = Boolean(state);
@@ -876,7 +855,7 @@ export default {
this.previousFocusedImage = null;
this.setFocusedImage(this.nextImageIndex);
this.autoScroll = true;
this.scrollHandler();
this.scrollToRight();
}
},
scrollToFocused() {
@@ -886,43 +865,28 @@ export default {
}
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
if (!domThumb) {
return;
if (domThumb) {
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
// separate scrollTo function had to be implemented since scrollIntoView
// caused undesirable behavior in layouts
// and could not simply be scoped to the parent element
if (this.isComposedInLayout) {
const wrapperWidth = this.$refs.thumbsWrapper.clientWidth ?? 0;
this.$refs.thumbsWrapper.scrollLeft = (
domThumb.offsetLeft - (wrapperWidth - domThumb.clientWidth) / 2);
return;
}
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
},
async scrollToRight() {
scrollToRight(type) {
if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
return;
}
const scrollWidth = this.$refs?.thumbsWrapper?.scrollWidth ?? 0;
const scrollWidth = this.$refs.thumbsWrapper.scrollWidth || 0;
if (!scrollWidth) {
return;
}
await Vue.nextTick();
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
},
scrollHandler() {
if (this.isPaused) {
return this.scrollToFocused();
} else if (this.autoScroll) {
return this.scrollToRight();
}
this.$nextTick(() => {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
},
matchIndexOfPreviousImage(previous, imageHistory) {
// match logic uses a composite of url and time to account
@@ -1123,7 +1087,7 @@ export default {
this.setSizedImageDimensions();
this.setImageViewport();
this.calculateViewHeight();
this.scrollHandler();
this.scrollToFocused();
},
setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
@@ -1158,7 +1122,9 @@ export default {
this.handleThumbWindowResizeEnded();
},
handleThumbWindowResizeEnded() {
this.scrollHandler();
if (!this.isPaused) {
this.scrollToRight('reset');
}
this.calculateViewHeight();
@@ -1171,6 +1137,7 @@ export default {
},
wheelZoom(e) {
e.preventDefault();
this.$refs.imageControls.wheelZoom(e);
},
startPan(e) {

View File

@@ -46,7 +46,7 @@ export default {
mounted() {
document.addEventListener('click', this.hideMenu);
},
destroyed() {
unmounted() {
document.removeEventListener('click', this.hideMenu);
},
methods: {

View File

@@ -28,7 +28,6 @@ function copyRelatedMetadata(metadata) {
return copiedMetadata;
}
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
export default class RelatedTelemetry {
constructor(openmct, domainObject, telemetryKeys) {
@@ -89,31 +88,9 @@ export default class RelatedTelemetry {
this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId);
this[key].requestLatestFor = async (datum) => {
// 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)
};
ephemeralContext.stopClock();
ephemeralContext.bounds(newBounds);
const options = {
start: newBounds.start,
end: newBounds.end,
timeContext: ephemeralContext,
start: this._openmct.time.bounds().start,
end: this._parseTime(datum),
strategy: 'latest'
};
let results = await this._openmct.telemetry

View File

@@ -194,9 +194,6 @@
overflow-y: hidden;
margin-bottom: 1px;
padding-bottom: $interiorMarginSm;
&.animate-scroll {
scroll-behavior: smooth;
}
}
&__auto-scroll-resume-button {

View File

@@ -50,7 +50,7 @@ export default {
this.telemetryCollection.on('clear', this.dataCleared);
this.telemetryCollection.load();
},
beforeDestroy() {
beforeUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;

View File

@@ -481,16 +481,19 @@ describe("The Imagery View Layouts", () => {
});
});
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
await Vue.nextTick();
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollHandler');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollHandler);
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
Vue.nextTick(() => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
Vue.nextTick(() => {
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
done();
});
});
});
xit('should change the image zoom factor when using the zoom buttons', async () => {
xit('should change the image zoom factor when using the zoom buttons', async (done) => {
await Vue.nextTick();
let imageSizeBefore;
let imageSizeAfter;
@@ -509,6 +512,7 @@ describe("The Imagery View Layouts", () => {
imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
done();
});
xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {
await Vue.nextTick();

View File

@@ -50,7 +50,7 @@
<Sidebar
ref="sidebar"
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="sidebarClasses"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:default-page-id="defaultPageId"
:selected-page-id="getSelectedPageId()"
:default-section-id="defaultSectionId"
@@ -123,7 +123,6 @@
</div>
<div
v-if="selectedPage && !selectedPage.isLocked"
:class="{ 'disabled': activeTransaction }"
class="c-notebook__drag-area icon-plus"
@click="newEntry()"
@dragover="dragOver"
@@ -134,11 +133,6 @@
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"
@@ -189,7 +183,6 @@ 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';
@@ -207,8 +200,7 @@ export default {
NotebookEntry,
Search,
SearchResults,
Sidebar,
ProgressBar
Sidebar
},
inject: ['agent', 'openmct', 'snapshotContainer'],
props: {
@@ -233,9 +225,7 @@ export default {
showNav: false,
sidebarCoversEntries: false,
filteredAndSortedEntries: [],
notebookAnnotations: {},
activeTransaction: false,
savingTransaction: false
notebookAnnotations: {}
};
},
computed: {
@@ -280,20 +270,6 @@ 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);
@@ -321,14 +297,12 @@ export default {
this.formatSidebar();
this.setSectionAndPageFromUrl();
this.transaction = null;
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
this.filterAndSortEntries();
this.unobserveEntries = this.openmct.objects.observe(this.domainObject, '*', this.filterAndSortEntries);
},
beforeDestroy() {
beforeUnmount() {
if (this.unlisten) {
this.unlisten();
}
@@ -775,7 +749,6 @@ export default {
return section.id;
},
async newEntry(embed = null) {
this.startTransaction();
this.resetSearch();
const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage);
@@ -918,34 +891,20 @@ export default {
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.activeTransaction = true;
this.transaction = this.openmct.objects.startTransaction();
}
},
async saveTransaction() {
if (this.transaction !== null) {
this.savingTransaction = true;
try {
await this.transaction.commit();
} finally {
this.endTransaction();
}
if (this.transaction !== undefined) {
await this.transaction.commit();
this.openmct.objects.endTransaction();
}
},
async cancelTransaction() {
if (this.transaction !== null) {
try {
await this.transaction.cancel();
} finally {
this.endTransaction();
}
if (this.transaction !== undefined) {
await this.transaction.cancel();
this.openmct.objects.endTransaction();
}
},
endTransaction() {
this.openmct.objects.endTransaction();
this.transaction = null;
this.savingTransaction = false;
this.activeTransaction = false;
}
}
};

View File

@@ -74,22 +74,19 @@ 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 remoteObject = await openmct.objects.get(localMutable.identifier, undefined, FORCE_REMOTE);
return applyLocalEntries(remoteObject, localEntries, openmct);
const remoteMutable = await openmct.objects.getMutable(localMutable.identifier);
applyLocalEntries(remoteMutable, localEntries, openmct);
openmct.objects.destroyMutable(remoteMutable);
}
return true;
}
function applyLocalEntries(remoteObject, entries, openmct) {
let shouldSave = false;
function applyLocalEntries(mutable, entries, openmct) {
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
const remoteEntries = remoteObject.configuration.entries[sectionKey][pageKey];
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
const mergedEntries = [].concat(remoteEntries);
let shouldMutate = false;
@@ -113,13 +110,8 @@ function applyLocalEntries(remoteObject, entries, openmct) {
});
if (shouldMutate) {
shouldSave = true;
openmct.objects.mutate(remoteObject, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
openmct.objects.mutate(mutable, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
}
});
});
if (shouldSave) {
return openmct.objects.save(remoteObject);
}
}

View File

@@ -5,16 +5,10 @@
:class="[severityClass]"
>
<span class="c-indicator__label">
<button
:aria-label="'Review ' + notificationsCountMessage(notifications.length)"
@click="toggleNotificationsList(true)"
>
<button @click="toggleNotificationsList(true)">
{{ notificationsCountMessage(notifications.length) }}
</button>
<button
aria-label="Clear all notifications"
@click="dismissAllNotifications()"
>
<button @click="dismissAllNotifications()">
Clear All
</button>
</span>

View File

@@ -1,7 +1,6 @@
<template>
<div
class="c-message"
role="listitem"
:class="'message-severity-' + notification.model.severity"
>
<div class="c-ne__time-and-content">
@@ -21,11 +20,6 @@
</div>
</div>
</div>
<button
:aria-label="'Dismiss notification of ' + notification.model.message"
class="c-click-icon c-overlay__close-button icon-x"
@click="dismiss()"
></button>
<div class="c-overlay__button-bar">
<button
v-for="(dialogOption, index) in notification.model.options"
@@ -58,14 +52,6 @@ export default {
notification: {
type: Object,
required: true
},
closeOverlay: {
type: Function,
required: true
},
notificationsCount: {
type: Number,
required: true
}
},
data() {
@@ -93,12 +79,6 @@ export default {
updateProgressBar(progressPerc, progressText) {
this.progressPerc = progressPerc;
this.progressText = progressText;
},
dismiss() {
this.notification.dismiss();
if (this.notificationsCount === 1) {
this.closeOverlay();
}
}
}
};

View File

@@ -6,16 +6,11 @@
{{ notificationsCountDisplayMessage(notifications.length) }}
</div>
</div>
<div
role="list"
class="w-messages c-overlay__messages"
>
<div class="w-messages c-overlay__messages">
<notification-message
v-for="notification in notifications"
:key="notification.model.timestamp"
:close-overlay="closeOverlay"
:notification="notification"
:notifications-count="notifications.length"
/>
</div>
</div>
@@ -62,9 +57,6 @@ export default {
}
});
},
closeOverlay() {
this.overlay.dismiss();
},
notificationsCountDisplayMessage(count) {
if (count > 1 || count === 0) {
return `Displaying ${count} notifications`;

View File

@@ -36,8 +36,8 @@ export default function () {
}
let wrappedFunction = openmct.objects.get;
openmct.objects.get = function migrate() {
return wrappedFunction.apply(openmct.objects, [...arguments])
openmct.objects.get = function migrate(identifier) {
return wrappedFunction.apply(openmct.objects, [identifier])
.then(function (object) {
if (needsMigration(object)) {
migrateObject(object)

View File

@@ -90,7 +90,7 @@ export default {
};
}
},
beforeDestroy() {
beforeUnmount() {
this.openmct.user.status.off('statusChange', this.setStatus);
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
},

View File

@@ -117,7 +117,7 @@ export default {
this.fetchStatusSummary();
this.openmct.user.status.on('statusChange', this.fetchStatusSummary);
},
beforeDestroy() {
beforeUnmount() {
this.openmct.user.status.off('statusChange', this.fetchStatusSummary);
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
},

View File

@@ -28,7 +28,6 @@
connected = false;
// stop listening for events
couchEventSource.removeEventListener('message', self.onCouchMessage);
couchEventSource.close();
console.debug('🚪 Closed couch connection 🚪');
return;

View File

@@ -96,13 +96,8 @@ 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 (this.openmct.objects.isTransactionActive()) {
isInTransaction = this.openmct.objects.transaction.getDirtyObject(objectIdentifier);
}
if (observersForObject && !isInTransaction) {
if (observersForObject) {
observersForObject.forEach(async (observer) => {
const updatedObject = await this.get(objectIdentifier);
if (this.isSynchronizedObject(updatedObject)) {
@@ -224,12 +219,7 @@ class CouchObjectProvider {
console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
} else {
if (body?.model && isNotebookOrAnnotationType(body.model)) {
// warn since we handle conflicts for notebooks
console.warn(error.message);
} else {
console.error(error.message);
}
console.error(error.message);
throw error;
}
@@ -244,8 +234,7 @@ class CouchObjectProvider {
#handleResponseCode(status, json, fetchOptions) {
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
if (status === CouchObjectProvider.HTTP_CONFLICT) {
const objectName = JSON.parse(fetchOptions.body)?.model?.name;
throw new this.openmct.objects.errors.Conflict(`Conflict persisting "${objectName}"`);
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
} else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {
if (!json.error || !json.reason) {
throw new Error(`CouchDB Error ${status}`);

View File

@@ -27,7 +27,7 @@
>
<template v-if="viewBounds && !options.compact">
<swim-lane>
<template slot="label">{{ timeSystem.name }}</template>
<slot name="label">{{ timeSystem.name }}</slot>
<timeline-axis
slot="object"
:bounds="viewBounds"
@@ -107,7 +107,7 @@ export default {
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
this.status = this.openmct.status.get(this.domainObject.identifier);
},
beforeDestroy() {
beforeUnmount() {
clearInterval(this.resizeTimer);
this.stopFollowingTimeContext();
if (this.unlisten) {

View File

@@ -65,7 +65,7 @@ export default {
this.openmct.selection.on('change', this.updateSelection);
this.openmct.time.on('timeSystem', this.setFormatters);
},
beforeDestroy() {
beforeUnmount() {
this.openmct.selection.off('change', this.updateSelection);
this.openmct.time.off('timeSystem', this.setFormatters);
},

View File

@@ -365,7 +365,7 @@ export default {
this.loaded = true;
},
beforeDestroy() {
beforeUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
this.destroy();

View File

@@ -131,7 +131,7 @@ export default {
this.listenTo(this.axis, 'change:key', this.updateTicksForceRegeneration, this);
this.updateTicks();
},
beforeDestroy() {
beforeUnmount() {
this.stopListening();
},
methods: {

View File

@@ -81,7 +81,7 @@ export default {
eventHelpers.extend(this);
this.imageExporter = new ImageExporter(this.openmct);
},
beforeDestroy() {
beforeUnmount() {
this.destroy();
},
methods: {

View File

@@ -89,7 +89,7 @@ export default {
this.openmct.time.on('timeSystem', this.syncXAxisToTimeSystem);
this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions);
},
beforeDestroy() {
beforeUnmount() {
this.openmct.time.off('timeSystem', this.syncXAxisToTimeSystem);
},
methods: {

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