Compare commits
7 Commits
refactor-c
...
hide-gripp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b117890f27 | ||
|
|
afc37209d2 | ||
|
|
e45b58e2a8 | ||
|
|
8b3487bdbe | ||
|
|
50719b1383 | ||
|
|
cfda067794 | ||
|
|
4847cc8931 |
@@ -1,176 +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"
|
||||
),
|
||||
"kdbush": path.join(projectRootDir, "node_modules/kdbush/kdbush.min.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: 27000000,
|
||||
maxAssetSize: 27000000
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -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,
|
||||
|
||||
@@ -1,27 +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 should be used to install the Example User
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.example.ExampleUser());
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
@@ -1,27 +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 should be used to install the Operator Status
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.OperatorStatus());
|
||||
});
|
||||
|
Before Width: | Height: | Size: 10 KiB |
@@ -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 }) => {
|
||||
@@ -49,11 +49,11 @@ test.describe('AppActions', () => {
|
||||
parent: e2eFolder.uuid
|
||||
});
|
||||
|
||||
await page.goto(timer1.url);
|
||||
await page.goto(timer1.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name);
|
||||
await page.goto(timer2.url);
|
||||
await page.goto(timer2.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name);
|
||||
await page.goto(timer3.url);
|
||||
await page.goto(timer3.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name);
|
||||
});
|
||||
|
||||
@@ -73,11 +73,11 @@ test.describe('AppActions', () => {
|
||||
name: 'Folder Baz',
|
||||
parent: folder2.uuid
|
||||
});
|
||||
await page.goto(folder1.url);
|
||||
await page.goto(folder1.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name);
|
||||
await page.goto(folder2.url);
|
||||
await page.goto(folder2.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name);
|
||||
await page.goto(folder3.url);
|
||||
await page.goto(folder3.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name);
|
||||
|
||||
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,8 +30,6 @@ 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 +68,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 }) => {
|
||||
|
||||
@@ -43,76 +43,48 @@ test.describe('Move & link item tests', () => {
|
||||
name: 'Child Folder',
|
||||
parent: parentFolder.uuid
|
||||
});
|
||||
const grandchildFolder = await createDomainObjectWithDefaults(page, {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Grandchild Folder',
|
||||
parent: childFolder.uuid
|
||||
});
|
||||
|
||||
// Attempt to move parent to its own grandparent
|
||||
await page.locator('button[title="Show selected item in tree"]').click();
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
|
||||
const treePane = page.locator('#tree-pane');
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: 'Parent Folder'
|
||||
}).click({
|
||||
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Move/
|
||||
}).click();
|
||||
|
||||
const locatorTree = page.locator('#locator-tree');
|
||||
const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
|
||||
const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: parentFolder.name
|
||||
});
|
||||
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
|
||||
const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
});
|
||||
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await childFolderLocatorTreeItem.click();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
|
||||
const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: grandchildFolder.name
|
||||
});
|
||||
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await grandchildFolderLocatorTreeItem.click();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('[aria-label="Cancel"]').click();
|
||||
|
||||
// Move Child Folder from Parent Folder to My Items
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
}).click({
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=1').click();
|
||||
|
||||
await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Move/
|
||||
}).click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
const myItemsPaneTreeItem = treePane.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
// Expect that Child Folder is in My Items, the root folder
|
||||
expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();
|
||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
|
||||
});
|
||||
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
@@ -142,7 +114,7 @@ test.describe('Move & link item tests', () => {
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton = page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled = await okButton.isDisabled();
|
||||
expect.soft(okButtonStateDisabled).toBeTruthy();
|
||||
|
||||
@@ -166,7 +138,7 @@ test.describe('Move & link item tests', () => {
|
||||
// See if it's possible to put the folder in the Telemetry object after creation
|
||||
await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton2 = page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled2 = await okButton2.isDisabled();
|
||||
expect(okButtonStateDisabled2).toBeTruthy();
|
||||
});
|
||||
@@ -186,76 +158,48 @@ test.describe('Move & link item tests', () => {
|
||||
name: 'Child Folder',
|
||||
parent: parentFolder.uuid
|
||||
});
|
||||
const grandchildFolder = await createDomainObjectWithDefaults(page, {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Grandchild Folder',
|
||||
parent: childFolder.uuid
|
||||
});
|
||||
|
||||
// Attempt to move parent to its own grandparent
|
||||
await page.locator('button[title="Show selected item in tree"]').click();
|
||||
// Attempt to link parent to its own grandparent
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
|
||||
const treePane = page.locator('#tree-pane');
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: 'Parent Folder'
|
||||
}).click({
|
||||
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Move/
|
||||
}).click();
|
||||
|
||||
const locatorTree = page.locator('#locator-tree');
|
||||
const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
|
||||
const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: parentFolder.name
|
||||
});
|
||||
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await page.locator('li.icon-link').click();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
|
||||
const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
});
|
||||
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await childFolderLocatorTreeItem.click();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
|
||||
const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: grandchildFolder.name
|
||||
});
|
||||
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await grandchildFolderLocatorTreeItem.click();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('[aria-label="Cancel"]').click();
|
||||
|
||||
// Move Child Folder from Parent Folder to My Items
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
}).click({
|
||||
// Link Child Folder from Parent Folder to My Items
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=1').click();
|
||||
|
||||
await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Link/
|
||||
}).click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
await page.locator('li.icon-link').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
const myItemsPaneTreeItem = treePane.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
// Expect that Child Folder is in My Items, the root folder
|
||||
expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();
|
||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,79 +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 { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
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
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Notification Overlay', () => {
|
||||
test('Closing notification list after notification banner disappeared does not cause it to open automatically', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6130'
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a new Display Layout object
|
||||
await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
|
||||
|
||||
// Click on the button "Review 1 Notification"
|
||||
await page.click('button[aria-label="Review 1 Notification"]');
|
||||
|
||||
// Verify that Notification List is open
|
||||
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true);
|
||||
|
||||
// Wait until there is no Notification Banner
|
||||
await page.waitForSelector('div[role="alert"]', { state: 'detached'});
|
||||
|
||||
// Click on the "Close" button of the Notification List
|
||||
await page.click('button[aria-label="Close"]');
|
||||
|
||||
// On the Display Layout object, click on the "Edit" button
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
// Click on the "Save" button
|
||||
await page.click('button[title="Save"]');
|
||||
|
||||
// Click on the "Save and Finish Editing" option
|
||||
await page.click('li[title="Save and Finish Editing"]');
|
||||
|
||||
// Verify that Notification List is NOT open
|
||||
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -98,8 +98,8 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
|
||||
|
||||
//Edit Condition Set Name from main view
|
||||
await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Unnamed Condition Set' }).first().fill('Renamed Condition Set');
|
||||
await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Renamed Condition Set' }).first().press('Enter');
|
||||
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
|
||||
await page.locator('text=Renamed Condition Set').first().press('Enter');
|
||||
// Click Save Button
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click Save and Finish Editing Option
|
||||
|
||||
@@ -24,7 +24,6 @@ const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
|
||||
|
||||
test.describe('Display Layout', () => {
|
||||
/** @type {import('../../../../appActions').CreatedObjectInfo} */
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
@@ -32,7 +31,8 @@ test.describe('Display Layout', () => {
|
||||
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator'
|
||||
type: 'Sine Wave Generator',
|
||||
name: "Test Sine Wave Generator"
|
||||
});
|
||||
});
|
||||
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
|
||||
@@ -47,12 +47,7 @@ test.describe('Display Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@@ -79,12 +74,7 @@ test.describe('Display Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@@ -115,12 +105,7 @@ test.describe('Display Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@@ -130,7 +115,7 @@ test.describe('Display Layout', () => {
|
||||
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
|
||||
|
||||
// Bring up context menu and remove
|
||||
await sineWaveGeneratorTreeItem.nth(1).click({ button: 'right' });
|
||||
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
@@ -145,7 +130,8 @@ test.describe('Display Layout', () => {
|
||||
});
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout'
|
||||
type: 'Display Layout',
|
||||
name: "Test Display Layout"
|
||||
});
|
||||
// Edit Display Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
@@ -153,12 +139,7 @@ test.describe('Display Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@@ -171,7 +152,7 @@ test.describe('Display Layout', () => {
|
||||
await page.goto(sineWaveObject.url);
|
||||
|
||||
// Bring up context menu and remove
|
||||
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
|
||||
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
|
||||
@@ -25,31 +25,26 @@ const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Flexible Layout', () => {
|
||||
let sineWaveObject;
|
||||
let clockObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator'
|
||||
type: 'Sine Wave Generator',
|
||||
name: "Test Sine Wave Generator"
|
||||
});
|
||||
|
||||
// Create Clock Object
|
||||
clockObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock'
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: "Test Clock"
|
||||
});
|
||||
});
|
||||
test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ page }) => {
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const clockTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(clockObject.name)
|
||||
});
|
||||
// Create a Flexible Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout'
|
||||
type: 'Flexible Layout',
|
||||
name: "Test Flexible Layout"
|
||||
});
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
@@ -57,8 +52,8 @@ test.describe('Flexible Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
|
||||
// Add the Sine Wave Generator and Clock to the Flexible Layout
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
|
||||
await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty');
|
||||
// Check that panes can be dragged while Flexible Layout is in Edit mode
|
||||
let dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
|
||||
await expect(dragWrapper).toHaveAttribute('draggable', 'true');
|
||||
@@ -70,13 +65,10 @@ test.describe('Flexible Layout', () => {
|
||||
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
|
||||
});
|
||||
test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ page }) => {
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout'
|
||||
type: 'Flexible Layout',
|
||||
name: "Test Flexible Layout"
|
||||
});
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
@@ -84,7 +76,7 @@ test.describe('Flexible Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
|
||||
// Add the Sine Wave Generator to the Flexible Layout and save changes
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@@ -94,7 +86,7 @@ test.describe('Flexible Layout', () => {
|
||||
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
|
||||
|
||||
// Bring up context menu and remove
|
||||
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
|
||||
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
@@ -106,14 +98,10 @@ test.describe('Flexible Layout', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/3117'
|
||||
});
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
|
||||
// Create a Flexible Layout
|
||||
const flexibleLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout'
|
||||
type: 'Flexible Layout',
|
||||
name: "Test Flexible Layout"
|
||||
});
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
@@ -121,7 +109,7 @@ test.describe('Flexible Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Flexible Layout and save changes
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@@ -134,7 +122,7 @@ test.describe('Flexible Layout', () => {
|
||||
await page.goto(sineWaveObject.url);
|
||||
|
||||
// Bring up context menu and remove
|
||||
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
|
||||
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
but only assume that example imagery is present.
|
||||
*/
|
||||
/* globals process */
|
||||
const { v4: uuid } = require('uuid');
|
||||
const { waitForAnimations } = require('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
|
||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
||||
const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
|
||||
|
||||
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
|
||||
test.describe('Example Imagery Object', () => {
|
||||
@@ -207,58 +207,6 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
await page.goto(displayLayout.url);
|
||||
});
|
||||
|
||||
test('View Large action pauses imagery when in realtime and returns to realtime', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/3647'
|
||||
});
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// set realtime mode
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
|
||||
// pause/play button
|
||||
const pausePlayButton = await page.locator('.c-button.pause-play');
|
||||
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
|
||||
// Open context menu and click view large menu item
|
||||
await page.locator('button[title="View menu items"]').click();
|
||||
await page.locator('li[title="View Large"]').click();
|
||||
await expect(pausePlayButton).toHaveClass(/is-paused/);
|
||||
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
test('View Large action leaves keeps realtime mode paused', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/3647'
|
||||
});
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// set realtime mode
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
|
||||
// pause/play button
|
||||
const pausePlayButton = await page.locator('.c-button.pause-play');
|
||||
await pausePlayButton.click();
|
||||
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
|
||||
|
||||
// Open context menu and click view large menu item
|
||||
await page.locator('button[title="View menu items"]').click();
|
||||
await page.locator('li[title="View Large"]').click();
|
||||
await expect(pausePlayButton).toHaveClass(/is-paused/);
|
||||
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
test('Imagery View operations @unstable', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
@@ -397,11 +345,13 @@ test.describe('Example Imagery in Time Strip', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
timeStripObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time Strip'
|
||||
type: 'Time Strip',
|
||||
name: 'Time Strip'.concat(' ', uuid())
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Imagery',
|
||||
name: 'Example Imagery'.concat(' ', uuid()),
|
||||
parent: timeStripObject.uuid
|
||||
});
|
||||
// Navigate to timestrip
|
||||
@@ -412,28 +362,17 @@ test.describe('Example Imagery in Time Strip', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5632'
|
||||
});
|
||||
|
||||
// Hover over the timestrip to reveal a thumbnail image
|
||||
await page.locator('.c-imagery-tsv-container').hover();
|
||||
|
||||
// Get the img src of the hovered image thumbnail
|
||||
const hoveredThumbnailImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
|
||||
const hoveredThumbnailImgSrc = await hoveredThumbnailImg.getAttribute('src');
|
||||
|
||||
// Verify that imagery timestrip view uses the thumbnailUrl as img src for thumbnails
|
||||
expect(hoveredThumbnailImgSrc).toBeTruthy();
|
||||
expect(hoveredThumbnailImgSrc).toMatch(thumbnailUrlParamsRegexp);
|
||||
|
||||
// Click on the hovered thumbnail to open "View Large" view
|
||||
// get url of the hovered image
|
||||
const hoveredImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
|
||||
const hoveredImgSrc = await hoveredImg.getAttribute('src');
|
||||
expect(hoveredImgSrc).toBeTruthy();
|
||||
await page.locator('.c-imagery-tsv-container').click();
|
||||
|
||||
// Get the img src of the large view image
|
||||
// get image of view large container
|
||||
const viewLargeImg = page.locator('img.c-imagery__main-image__image');
|
||||
const viewLargeImgSrc = await viewLargeImg.getAttribute('src');
|
||||
expect(viewLargeImgSrc).toBeTruthy();
|
||||
|
||||
// Verify that the image in the large view is the same as the hovered thumbnail
|
||||
expect(viewLargeImgSrc).toEqual(hoveredThumbnailImgSrc.split('?')[0]);
|
||||
expect(viewLargeImgSrc).toEqual(hoveredImgSrc);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -450,12 +389,6 @@ test.describe('Example Imagery in Time Strip', () => {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function performImageryViewOperationsAndAssert(page) {
|
||||
// Verify that imagery thumbnails use a thumbnail url
|
||||
const thumbnailImages = page.locator('.c-thumb__image');
|
||||
const mainImage = page.locator('.c-imagery__main-image__image');
|
||||
await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
||||
await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
||||
|
||||
// Click previous image button
|
||||
const previousImageButton = page.locator('.c-nav--prev');
|
||||
await previousImageButton.click();
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
|
||||
*/
|
||||
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const nbUtils = require('../../../../helper/notebookUtils');
|
||||
@@ -263,77 +265,71 @@ test.describe('Notebook entry tests', () => {
|
||||
});
|
||||
test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
|
||||
test.fixme('previous and new entries can be deleted', async ({ page }) => {});
|
||||
test.fixme('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
|
||||
const TEST_LINK = 'http://www.google.com';
|
||||
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
// Create Notebook
|
||||
const notebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: "Entry Link Test"
|
||||
});
|
||||
|
||||
await expandTreePaneItemByName(page, 'My Items');
|
||||
|
||||
await page.goto(notebook.url);
|
||||
|
||||
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
|
||||
|
||||
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
|
||||
|
||||
// Start waiting for popup before clicking. Note no await.
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
|
||||
await validLink.click();
|
||||
const popup = await popupPromise;
|
||||
|
||||
// Wait for the popup to load.
|
||||
await popup.waitForLoadState();
|
||||
expect.soft(popup.url()).toContain('www.google.com');
|
||||
|
||||
expect(await validLink.count()).toBe(1);
|
||||
test.describe('Snapshot Menu tests', () => {
|
||||
test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => {
|
||||
// There should be no default notebook
|
||||
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
|
||||
// refresh page
|
||||
// Click on 'Notebook Snaphot Menu'
|
||||
// 'save to Notebook Snapshots' should be only option there
|
||||
});
|
||||
test.fixme('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => {
|
||||
const TEST_LINK = 'www.google.com';
|
||||
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create Notebook
|
||||
const notebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: "Entry Link Test"
|
||||
});
|
||||
|
||||
await expandTreePaneItemByName(page, 'My Items');
|
||||
|
||||
await page.goto(notebook.url);
|
||||
|
||||
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
|
||||
|
||||
const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
|
||||
|
||||
expect(await invalidLink.count()).toBe(0);
|
||||
test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
|
||||
// Create 2a notebooks
|
||||
// Set Notebook A as Default
|
||||
// Open Snapshot Menu and note that Notebook A is listed
|
||||
// Close Snapshot Menu
|
||||
// Set Default Notebook to Notebook B
|
||||
// Open Snapshot Notebook and note that Notebook B is listed
|
||||
// Select Default Notebook Option and verify that Snapshot is added to Notebook B
|
||||
});
|
||||
test.fixme('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ page }) => {
|
||||
const TEST_LINK = 'http://www.google.com?bad=';
|
||||
const TEST_LINK_BAD = `http://www.google.com?bad=<script>alert('gimme your cookies')</script>`;
|
||||
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create Notebook
|
||||
const notebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: "Entry Link Test"
|
||||
});
|
||||
|
||||
await expandTreePaneItemByName(page, 'My Items');
|
||||
|
||||
await page.goto(notebook.url);
|
||||
|
||||
await nbUtils.enterTextEntry(page, `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`);
|
||||
|
||||
const sanitizedLink = page.locator(`a[href="${TEST_LINK}"]`);
|
||||
const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`);
|
||||
|
||||
expect.soft(await sanitizedLink.count()).toBe(1);
|
||||
expect(await unsanitizedLink.count()).toBe(0);
|
||||
test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
|
||||
//Note this should be a visual test, too
|
||||
// Create Telemetry object
|
||||
// Create A notebook with many pages and sections.
|
||||
// Set page and section defaults to be between first and last of many. i.e. 3 of 5
|
||||
// Navigate to Telemetry object
|
||||
// Select Default Notebook Option and verify that Snapshot is added to Notebook A
|
||||
// Verify Snapshot Details appear correctly
|
||||
});
|
||||
test.fixme('Snapshots adjust time conductor', async ({ page }) => {
|
||||
// Create Telemetry object
|
||||
// Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
|
||||
// Embed Telemetry object into notebook
|
||||
// Set Time Conductor to Local clock
|
||||
// Click into embedded telemetry object and verify object appears with same fixed time from record
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Snapshot Container tests', () => {
|
||||
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
|
||||
test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
|
||||
test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {});
|
||||
test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {});
|
||||
test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});
|
||||
test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
|
||||
//Create Notebook
|
||||
//Create Telemetry Object
|
||||
//From Telemetry Object, use 'save to Notebook Snapshots'
|
||||
//Snapshots indicator should blink, click on it to view snapshots
|
||||
//Navigate to Notebook
|
||||
//Drag and Drop onto droppable area for new entry
|
||||
//New Entry created with given snapshot added
|
||||
//Snapshot removed from container?
|
||||
});
|
||||
test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => {
|
||||
//Create Notebook
|
||||
//Create Telemetry Object
|
||||
//From Telemetry Object, use 'save to Notebook Snapshots'
|
||||
//Snapshots indicator should blink, click on it to view snapshots
|
||||
//Navigate to Notebook
|
||||
//Drag and Drop into exiting entry
|
||||
//Existing Entry updated with given snapshot
|
||||
//Snapshot removed from container?
|
||||
});
|
||||
test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
|
||||
//Add snapshot to container
|
||||
//Verify PNG, JPG, and Annotate buttons work correctly
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
// const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
// const nbUtils = require('../../../../helper/notebookUtils');
|
||||
|
||||
test.describe('Snapshot Menu tests', () => {
|
||||
test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => {
|
||||
// There should be no default notebook
|
||||
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
|
||||
// refresh page
|
||||
// Click on 'Notebook Snaphot Menu'
|
||||
// 'save to Notebook Snapshots' should be only option there
|
||||
});
|
||||
test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
|
||||
// Create 2a notebooks
|
||||
// Set Notebook A as Default
|
||||
// Open Snapshot Menu and note that Notebook A is listed
|
||||
// Close Snapshot Menu
|
||||
// Set Default Notebook to Notebook B
|
||||
// Open Snapshot Notebook and note that Notebook B is listed
|
||||
// Select Default Notebook Option and verify that Snapshot is added to Notebook B
|
||||
});
|
||||
test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
|
||||
//Note this should be a visual test, too
|
||||
// Create Telemetry object
|
||||
// Create A notebook with many pages and sections.
|
||||
// Set page and section defaults to be between first and last of many. i.e. 3 of 5
|
||||
// Navigate to Telemetry object
|
||||
// Select Default Notebook Option and verify that Snapshot is added to Notebook A
|
||||
// Verify Snapshot Details appear correctly
|
||||
});
|
||||
test.fixme('Snapshots adjust time conductor', async ({ page }) => {
|
||||
// Create Telemetry object
|
||||
// Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
|
||||
// Embed Telemetry object into notebook
|
||||
// Set Time Conductor to Local clock
|
||||
// Click into embedded telemetry object and verify object appears with same fixed time from record
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Snapshot Container tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create Notebook
|
||||
// const notebook = await createDomainObjectWithDefaults(page, {
|
||||
// type: 'Notebook',
|
||||
// name: "Test Notebook"
|
||||
// });
|
||||
// // Create Overlay Plot
|
||||
// const snapShotObject = await createDomainObjectWithDefaults(page, {
|
||||
// type: 'Overlay Plot',
|
||||
// name: "Dropped Overlay Plot"
|
||||
// });
|
||||
|
||||
await page.getByRole('button', { name: ' Snapshot ' }).click();
|
||||
await page.getByRole('menuitem', { name: ' Save to Notebook Snapshots' }).click();
|
||||
await page.getByRole('button', { name: 'Show' }).click();
|
||||
|
||||
});
|
||||
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
|
||||
test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
|
||||
test.fixme('A snapshot can be Deleted from Container with 3 dot action menu', async ({ page }) => {});
|
||||
test.fixme('A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', async ({ page }) => {
|
||||
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click();
|
||||
await page.getByRole('menuitem', { name: ' View Snapshot' }).click();
|
||||
await expect(page.locator('.c-overlay__outer')).toBeVisible();
|
||||
await page.getByTitle('Annotate').click();
|
||||
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
|
||||
await page.getByRole('button', { name: '' }).click();
|
||||
// await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
//await expect(await page.locator)
|
||||
});
|
||||
test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => {
|
||||
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click();
|
||||
await page.getByRole('menuitem', { name: 'Quick View' }).click();
|
||||
await expect(page.locator('.c-overlay__outer')).toBeVisible();
|
||||
});
|
||||
test.fixme('A snapshot can be Navigated To from Container with 3 dot action menu', async ({ page }) => {});
|
||||
test.fixme('A snapshot can be Navigated To Item in Time from Container with 3 dot action menu', async ({ page }) => {});
|
||||
test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});
|
||||
test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
|
||||
//Create Notebook
|
||||
//Create Telemetry Object
|
||||
//From Telemetry Object, use 'save to Notebook Snapshots'
|
||||
//Snapshots indicator should blink, click on it to view snapshots
|
||||
//Navigate to Notebook
|
||||
//Drag and Drop onto droppable area for new entry
|
||||
//New Entry created with given snapshot added
|
||||
//Snapshot removed from container?
|
||||
});
|
||||
test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => {
|
||||
//Create Notebook
|
||||
//Create Telemetry Object
|
||||
//From Telemetry Object, use 'save to Notebook Snapshots'
|
||||
//Snapshots indicator should blink, click on it to view snapshots
|
||||
//Navigate to Notebook
|
||||
//Drag and Drop into exiting entry
|
||||
//Existing Entry updated with given snapshot
|
||||
//Snapshot removed from container?
|
||||
});
|
||||
test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
|
||||
//Add snapshot to container
|
||||
//Verify PNG, JPG, and Annotate buttons work correctly
|
||||
});
|
||||
});
|
||||
@@ -76,7 +76,6 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
|
||||
await page.waitForLoadState('networkidle');
|
||||
expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2);
|
||||
|
||||
@@ -149,17 +148,14 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').fill(`Second Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').fill(`Third Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').press('Enter');
|
||||
|
||||
// Add three tags
|
||||
await page.hover(`button:has-text("Add Tag") >> nth=2`);
|
||||
|
||||
@@ -152,7 +152,7 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
|
||||
|
||||
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
|
||||
// Click .c-ne__embed__name .c-popup-menu-button
|
||||
await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
|
||||
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
|
||||
|
||||
const embedMenu = page.locator('body >> .c-menu');
|
||||
await expect(embedMenu).toContainText('Remove This Embed');
|
||||
@@ -161,7 +161,7 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
|
||||
test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
|
||||
await lockPage(page);
|
||||
// Click .c-ne__embed__name .c-popup-menu-button
|
||||
await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
|
||||
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
|
||||
|
||||
const embedMenu = page.locator('body >> .c-menu');
|
||||
await expect(embedMenu).not.toContainText('Remove This Embed');
|
||||
|
||||
@@ -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;
|
||||
@@ -57,14 +56,12 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
||||
*/
|
||||
async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
const notebook = await createNotebookAndEntry(page, iterations);
|
||||
await page.locator('text=Annotations').click();
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Hover and click "Add Tag" button
|
||||
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||
await page.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click();
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
@@ -73,8 +70,8 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
|
||||
// Hover and click "Add Tag" button
|
||||
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Select the "Science" tag
|
||||
@@ -86,10 +83,8 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
|
||||
test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test('Can load tags', async ({ page }) => {
|
||||
|
||||
await createNotebookAndEntry(page);
|
||||
|
||||
await page.locator('text=Annotations').click();
|
||||
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
@@ -130,12 +125,13 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
|
||||
test('Can delete tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
||||
// Delete Driving
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||
@@ -197,16 +193,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
page.goto('./#/browse/mine?hideTree=false'),
|
||||
page.click('.c-disclosure-triangle')
|
||||
]);
|
||||
|
||||
const treePane = page.locator('#tree-pane');
|
||||
// Click Clock
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: clock.name
|
||||
}).click();
|
||||
await page.click(`text=${clock.name}`);
|
||||
|
||||
// Click Notebook
|
||||
await page.getByRole('treeitem', {
|
||||
name: notebook.name
|
||||
}).click();
|
||||
await page.click(`text=${notebook.name}`);
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
|
||||
@@ -1,156 +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 testing the operator status plugin.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
|
||||
/*
|
||||
|
||||
Precondition: Inject Example User, Operator Status Plugins
|
||||
Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation)
|
||||
|
||||
Clear Role Status of single user test
|
||||
STUB (test.fixme) Rolling through each
|
||||
|
||||
*/
|
||||
|
||||
test.describe('Operator Status', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// FIXME: determine if plugins will be added to index.html or need to be injected
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js')});
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')});
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
// verify that operator status is visible
|
||||
test('operator status is visible and expands when clicked', async ({ page }) => {
|
||||
await expect(page.locator('div[title="Set my operator status"]')).toBeVisible();
|
||||
await page.locator('div[title="Set my operator status"]').click();
|
||||
|
||||
// expect default status to be 'GO'
|
||||
await expect(page.locator('.c-status-poll-panel')).toBeVisible();
|
||||
});
|
||||
|
||||
test('poll question indicator remains when blank poll set', async ({ page }) => {
|
||||
await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible();
|
||||
await page.locator('div[title="Set the current poll question"]').click();
|
||||
// set to blank
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
// should still be visible
|
||||
await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible();
|
||||
});
|
||||
|
||||
// Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation)
|
||||
test('operator status table reflects answered values', async ({ page }) => {
|
||||
// user navigates to operator status poll
|
||||
const statusPollIndicator = page.locator('div[title="Set my operator status"]');
|
||||
await statusPollIndicator.click();
|
||||
|
||||
// get user role value
|
||||
const userRole = page.locator('.c-status-poll-panel__user-role');
|
||||
const userRoleText = await userRole.innerText();
|
||||
|
||||
// get selected status value
|
||||
const selectStatus = page.locator('select[name="setStatus"]');
|
||||
await selectStatus.selectOption({ index: 1});
|
||||
const initialStatusValue = await selectStatus.inputValue();
|
||||
|
||||
// open manage status poll
|
||||
const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]');
|
||||
await manageStatusPollIndicator.click();
|
||||
// parse the table row values
|
||||
const row = page.locator(`tr:has-text("${userRoleText}")`);
|
||||
const rowValues = await row.innerText();
|
||||
const rowValuesArr = rowValues.split('\t');
|
||||
const COLUMN_STATUS_INDEX = 1;
|
||||
// check initial set value matches status table
|
||||
expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
|
||||
.toEqual(initialStatusValue.toLowerCase());
|
||||
|
||||
// change user status
|
||||
await statusPollIndicator.click();
|
||||
// FIXME: might want to grab a dynamic option instead of arbitrary
|
||||
await page.locator('select[name="setStatus"]').selectOption({ index: 2});
|
||||
const updatedStatusValue = await selectStatus.inputValue();
|
||||
// verify user status is reflected in table
|
||||
await manageStatusPollIndicator.click();
|
||||
|
||||
const updatedRow = page.locator(`tr:has-text("${userRoleText}")`);
|
||||
const updatedRowValues = await updatedRow.innerText();
|
||||
const updatedRowValuesArr = updatedRowValues.split('\t');
|
||||
|
||||
expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
|
||||
.toEqual(updatedStatusValue.toLowerCase());
|
||||
|
||||
});
|
||||
|
||||
test('clear poll button removes poll responses', async ({ page }) => {
|
||||
// user navigates to operator status poll
|
||||
const statusPollIndicator = page.locator('div[title="Set my operator status"]');
|
||||
await statusPollIndicator.click();
|
||||
|
||||
// get user role value
|
||||
const userRole = page.locator('.c-status-poll-panel__user-role');
|
||||
const userRoleText = await userRole.innerText();
|
||||
|
||||
// get selected status value
|
||||
const selectStatus = page.locator('select[name="setStatus"]');
|
||||
// FIXME: might want to grab a dynamic option instead of arbitrary
|
||||
await selectStatus.selectOption({ index: 1});
|
||||
const initialStatusValue = await selectStatus.inputValue();
|
||||
|
||||
// open manage status poll
|
||||
const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]');
|
||||
await manageStatusPollIndicator.click();
|
||||
// parse the table row values
|
||||
const row = page.locator(`tr:has-text("${userRoleText}")`);
|
||||
const rowValues = await row.innerText();
|
||||
const rowValuesArr = rowValues.split('\t');
|
||||
const COLUMN_STATUS_INDEX = 1;
|
||||
// check initial set value matches status table
|
||||
expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
|
||||
.toEqual(initialStatusValue.toLowerCase());
|
||||
|
||||
// clear the poll
|
||||
await page.locator('button[title="Clear the previous poll question"]').click();
|
||||
|
||||
const updatedRow = page.locator(`tr:has-text("${userRoleText}")`);
|
||||
const updatedRowValues = await updatedRow.innerText();
|
||||
const updatedRowValuesArr = updatedRowValues.split('\t');
|
||||
const UNSET_VALUE_LABEL = 'Not set';
|
||||
expect(updatedRowValuesArr[COLUMN_STATUS_INDEX])
|
||||
.toEqual(UNSET_VALUE_LABEL);
|
||||
|
||||
});
|
||||
|
||||
test.fixme('iterate through all possible response values', async ({ page }) => {
|
||||
// test all possible respone values for the poll
|
||||
});
|
||||
|
||||
});
|
||||
@@ -32,7 +32,7 @@ test.use({
|
||||
}
|
||||
});
|
||||
|
||||
test.fixme('ExportAsJSON', () => {
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 21 KiB |
@@ -97,28 +97,20 @@ test.describe('Overlay Plot', () => {
|
||||
await page.mouse.move(0, 100);
|
||||
await page.mouse.up();
|
||||
|
||||
// Drag swg a, c, e into Y Axis 2
|
||||
await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
|
||||
// Drag swg b into Y Axis 3
|
||||
await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]'));
|
||||
|
||||
const yAxis1Group = page.getByLabel("Y Axis 1");
|
||||
const yAxis2Group = page.getByLabel("Y Axis 2");
|
||||
const yAxis3Group = page.getByLabel("Y Axis 3");
|
||||
|
||||
// Verify that the elements are in the correct buckets and in the correct order
|
||||
expect(yAxis1Group.getByRole('listitem', { name: 'swg d' })).toBeTruthy();
|
||||
expect(yAxis1Group.getByRole('listitem').nth(0).getByText('swg d')).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: 'swg e' })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(0).getByText('swg e')).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: 'swg c' })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(1).getByText('swg c')).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: 'swg a' })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(2).getByText('swg a')).toBeTruthy();
|
||||
expect(yAxis3Group.getByRole('listitem', { name: 'swg b' })).toBeTruthy();
|
||||
expect(yAxis3Group.getByRole('listitem').nth(0).getByText('swg b')).toBeTruthy();
|
||||
await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
|
||||
await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
|
||||
await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
|
||||
await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group"]').nth(2));
|
||||
const elementsTree = await page.locator('#inspector-elements-tree').allInnerTexts();
|
||||
expect(elementsTree.join('').split('\n')).toEqual([
|
||||
"Y Axis 1",
|
||||
"swg d",
|
||||
"Y Axis 2",
|
||||
"swg e",
|
||||
"swg c",
|
||||
"swg a",
|
||||
"Y Axis 3",
|
||||
"swg b"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,85 +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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||
|
||||
test.describe('Recent Objects', () => {
|
||||
test('Recent Objects CRUD operations', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a folder and nest a Clock within it
|
||||
const recentObjectsList = page.locator('[aria-label="Recent Objects"]');
|
||||
const folderA = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
});
|
||||
const clock = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
parent: folderA.uuid
|
||||
});
|
||||
|
||||
// Drag the Recent Objects panel up a bit
|
||||
await page.locator('div:nth-child(2) > .l-pane__handle').hover();
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(0, 100);
|
||||
await page.mouse.up();
|
||||
|
||||
// Verify that both created objects appear in the list and are in the correct order
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy();
|
||||
|
||||
// Navigate to the folder by clicking on the main object name in the recent objects list item
|
||||
await recentObjectsList.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
|
||||
await page.waitForURL(`**/${folderA.uuid}?*`);
|
||||
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();
|
||||
|
||||
// Rename
|
||||
folderA.name = `${folderA.name}-NEW!`;
|
||||
await page.locator('.l-browse-bar__object-name').fill("");
|
||||
await page.locator('.l-browse-bar__object-name').fill(folderA.name);
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// Verify rename has been applied in recent objects list item and objects paths
|
||||
expect(page.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
|
||||
|
||||
// Delete
|
||||
await page.click('button[title="Show selected item in tree"]');
|
||||
// Delete the folder via the left tree pane treeitem context menu
|
||||
await page.getByRole('treeitem', { name: new RegExp(folderA.name) }).locator('a').click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByRole('menuitem', { name: /Remove/ }).click();
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
|
||||
// Verify that the folder and clock are no longer in the recent objects list
|
||||
await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
|
||||
await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
|
||||
});
|
||||
test.fixme("Clicking on the 'target button' scrolls the object into view in the tree and highlights it");
|
||||
test.fixme("Clicking on an object in the path of a recent object navigates to the object");
|
||||
test.fixme("Tests for context menu actions from recent objects");
|
||||
});
|
||||
@@ -72,7 +72,7 @@ test.describe('Grand Search', () => {
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click()
|
||||
page.locator('text=Clock A').click()
|
||||
]);
|
||||
await expect(page.locator('.is-object-type-clock')).toBeVisible();
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -65,7 +65,7 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
this.user = undefined;
|
||||
this.loggedIn = false;
|
||||
this.autoLoginUser = undefined;
|
||||
this.status = STATUSES[0];
|
||||
this.status = STATUSES[1];
|
||||
this.pollQuestion = undefined;
|
||||
this.defaultStatusRole = defaultStatusRole;
|
||||
|
||||
@@ -124,7 +124,6 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
}
|
||||
|
||||
setStatusForRole(role, status) {
|
||||
status.timestamp = Date.now();
|
||||
this.status = status;
|
||||
this.emit('statusChange', {
|
||||
role,
|
||||
@@ -134,23 +133,14 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
async getPollQuestion() {
|
||||
if (this.pollQuestion) {
|
||||
return this.pollQuestion;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
getPollQuestion() {
|
||||
return Promise.resolve({
|
||||
question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
setPollQuestion(pollQuestion) {
|
||||
if (!pollQuestion) {
|
||||
// If the poll question is undefined, set it to a blank string.
|
||||
// This behavior better reflects how other telemetry systems
|
||||
// deal with undefined poll questions.
|
||||
pollQuestion = '';
|
||||
}
|
||||
|
||||
this.pollQuestion = {
|
||||
question: pollQuestion,
|
||||
timestamp: Date.now()
|
||||
|
||||
@@ -37,9 +37,8 @@ define([
|
||||
infinityValues: false
|
||||
};
|
||||
|
||||
function GeneratorProvider(openmct, StalenessProvider) {
|
||||
this.openmct = openmct;
|
||||
this.workerInterface = new WorkerInterface(openmct, StalenessProvider);
|
||||
function GeneratorProvider(openmct) {
|
||||
this.workerInterface = new WorkerInterface(openmct);
|
||||
}
|
||||
|
||||
GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
|
||||
@@ -82,7 +81,6 @@ define([
|
||||
workerRequest[prop] = Number(workerRequest[prop]);
|
||||
});
|
||||
|
||||
workerRequest.id = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
workerRequest.name = domainObject.name;
|
||||
|
||||
return workerRequest;
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
export default class SinewaveLimitProvider extends EventEmitter {
|
||||
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.observingStaleness = {};
|
||||
this.watchingTheClock = false;
|
||||
this.isRealTime = undefined;
|
||||
}
|
||||
|
||||
supportsStaleness(domainObject) {
|
||||
return domainObject.type === 'generator';
|
||||
}
|
||||
|
||||
isStale(domainObject, options) {
|
||||
if (!this.providingStaleness(domainObject)) {
|
||||
return Promise.resolve({
|
||||
isStale: false,
|
||||
utc: 0
|
||||
});
|
||||
}
|
||||
|
||||
const id = this.getObjectKeyString(domainObject);
|
||||
|
||||
if (!this.observerExists(id)) {
|
||||
this.createObserver(id);
|
||||
}
|
||||
|
||||
return Promise.resolve(this.observingStaleness[id].isStale);
|
||||
}
|
||||
|
||||
subscribeToStaleness(domainObject, callback) {
|
||||
const id = this.getObjectKeyString(domainObject);
|
||||
|
||||
if (this.isRealTime === undefined) {
|
||||
this.updateRealTime(this.openmct.time.clock());
|
||||
}
|
||||
|
||||
this.handleClockUpdate();
|
||||
|
||||
if (this.observerExists(id)) {
|
||||
this.addCallbackToObserver(id, callback);
|
||||
} else {
|
||||
this.createObserver(id, callback);
|
||||
}
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
if (this.providingStaleness(domainObject)) {
|
||||
this.updateStaleness(id, !this.observingStaleness[id].isStale);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
this.updateStaleness(id, false);
|
||||
this.handleClockUpdate();
|
||||
this.destroyObserver(id);
|
||||
};
|
||||
}
|
||||
|
||||
handleClockUpdate() {
|
||||
let observers = Object.values(this.observingStaleness).length > 0;
|
||||
|
||||
if (observers && !this.watchingTheClock) {
|
||||
this.watchingTheClock = true;
|
||||
this.openmct.time.on('clock', this.updateRealTime, this);
|
||||
} else if (!observers && this.watchingTheClock) {
|
||||
this.watchingTheClock = false;
|
||||
this.openmct.time.off('clock', this.updateRealTime, this);
|
||||
}
|
||||
}
|
||||
|
||||
updateRealTime(clock) {
|
||||
this.isRealTime = clock !== undefined;
|
||||
|
||||
if (!this.isRealTime) {
|
||||
Object.keys(this.observingStaleness).forEach((id) => {
|
||||
this.updateStaleness(id, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateStaleness(id, isStale) {
|
||||
this.observingStaleness[id].isStale = isStale;
|
||||
this.observingStaleness[id].utc = Date.now();
|
||||
this.observingStaleness[id].callback({
|
||||
isStale: this.observingStaleness[id].isStale,
|
||||
utc: this.observingStaleness[id].utc
|
||||
});
|
||||
this.emit('stalenessEvent', {
|
||||
id,
|
||||
isStale: this.observingStaleness[id].isStale
|
||||
});
|
||||
}
|
||||
|
||||
createObserver(id, callback) {
|
||||
this.observingStaleness[id] = {
|
||||
isStale: false,
|
||||
utc: Date.now()
|
||||
};
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
this.addCallbackToObserver(id, callback);
|
||||
}
|
||||
}
|
||||
|
||||
destroyObserver(id) {
|
||||
delete this.observingStaleness[id];
|
||||
}
|
||||
|
||||
providingStaleness(domainObject) {
|
||||
return domainObject.telemetry?.staleness === true && this.isRealTime;
|
||||
}
|
||||
|
||||
getObjectKeyString(object) {
|
||||
return this.openmct.objects.makeKeyString(object.identifier);
|
||||
}
|
||||
|
||||
addCallbackToObserver(id, callback) {
|
||||
this.observingStaleness[id].callback = callback;
|
||||
}
|
||||
|
||||
observerExists(id) {
|
||||
return this.observingStaleness?.[id];
|
||||
}
|
||||
}
|
||||
@@ -25,24 +25,14 @@ define([
|
||||
], function (
|
||||
{ v4: uuid }
|
||||
) {
|
||||
function WorkerInterface(openmct, StalenessProvider) {
|
||||
function WorkerInterface(openmct) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`;
|
||||
this.StalenessProvider = StalenessProvider;
|
||||
this.worker = new Worker(workerUrl);
|
||||
this.worker.onmessage = this.onMessage.bind(this);
|
||||
this.callbacks = {};
|
||||
this.staleTelemetryIds = {};
|
||||
|
||||
this.watchStaleness();
|
||||
}
|
||||
|
||||
WorkerInterface.prototype.watchStaleness = function () {
|
||||
this.StalenessProvider.on('stalenessEvent', ({ id, isStale}) => {
|
||||
this.staleTelemetryIds[id] = isStale;
|
||||
});
|
||||
};
|
||||
|
||||
WorkerInterface.prototype.onMessage = function (message) {
|
||||
message = message.data;
|
||||
var callback = this.callbacks[message.id];
|
||||
@@ -93,12 +83,11 @@ define([
|
||||
};
|
||||
|
||||
WorkerInterface.prototype.subscribe = function (request, cb) {
|
||||
const id = request.id;
|
||||
const messageId = this.dispatch('subscribe', request, (message) => {
|
||||
if (!this.staleTelemetryIds[id]) {
|
||||
cb(message.data);
|
||||
}
|
||||
});
|
||||
function callback(message) {
|
||||
cb(message.data);
|
||||
}
|
||||
|
||||
var messageId = this.dispatch('subscribe', request, callback);
|
||||
|
||||
return function () {
|
||||
this.dispatch('unsubscribe', {
|
||||
|
||||
@@ -20,163 +20,158 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import GeneratorProvider from "./GeneratorProvider";
|
||||
import SinewaveLimitProvider from "./SinewaveLimitProvider";
|
||||
import SinewaveStalenessProvider from "./SinewaveStalenessProvider";
|
||||
import StateGeneratorProvider from "./StateGeneratorProvider";
|
||||
import GeneratorMetadataProvider from "./GeneratorMetadataProvider";
|
||||
define([
|
||||
"./GeneratorProvider",
|
||||
"./SinewaveLimitProvider",
|
||||
"./StateGeneratorProvider",
|
||||
"./GeneratorMetadataProvider"
|
||||
], function (
|
||||
GeneratorProvider,
|
||||
SinewaveLimitProvider,
|
||||
StateGeneratorProvider,
|
||||
GeneratorMetadataProvider
|
||||
) {
|
||||
|
||||
export default function (openmct) {
|
||||
return function (openmct) {
|
||||
|
||||
openmct.types.addType("example.state-generator", {
|
||||
name: "State Generator",
|
||||
description: "For development use. Generates example enumerated telemetry by cycling through a given set of states.",
|
||||
cssClass: "icon-generator-telemetry",
|
||||
creatable: true,
|
||||
form: [
|
||||
{
|
||||
name: "State Duration (seconds)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "duration",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"duration"
|
||||
]
|
||||
openmct.types.addType("example.state-generator", {
|
||||
name: "State Generator",
|
||||
description: "For development use. Generates example enumerated telemetry by cycling through a given set of states.",
|
||||
cssClass: "icon-generator-telemetry",
|
||||
creatable: true,
|
||||
form: [
|
||||
{
|
||||
name: "State Duration (seconds)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "duration",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"duration"
|
||||
]
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
duration: 5
|
||||
};
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
duration: 5
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
openmct.telemetry.addProvider(new StateGeneratorProvider());
|
||||
openmct.telemetry.addProvider(new StateGeneratorProvider());
|
||||
|
||||
openmct.types.addType("generator", {
|
||||
name: "Sine Wave Generator",
|
||||
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
|
||||
cssClass: "icon-generator-telemetry",
|
||||
creatable: true,
|
||||
form: [
|
||||
{
|
||||
name: "Period",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "period",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"period"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Amplitude",
|
||||
control: "numberfield",
|
||||
cssClass: "l-numeric",
|
||||
key: "amplitude",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"amplitude"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Offset",
|
||||
control: "numberfield",
|
||||
cssClass: "l-numeric",
|
||||
key: "offset",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"offset"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Data Rate (hz)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "dataRateInHz",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"dataRateInHz"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Phase (radians)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "phase",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"phase"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Randomness",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "randomness",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"randomness"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Loading Delay (ms)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "loadDelay",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"loadDelay"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Include Infinity Values",
|
||||
control: "toggleSwitch",
|
||||
cssClass: "l-input",
|
||||
key: "infinityValues",
|
||||
property: [
|
||||
"telemetry",
|
||||
"infinityValues"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Provide Staleness Updates",
|
||||
control: "toggleSwitch",
|
||||
cssClass: "l-input",
|
||||
key: "staleness",
|
||||
property: [
|
||||
"telemetry",
|
||||
"staleness"
|
||||
]
|
||||
openmct.types.addType("generator", {
|
||||
name: "Sine Wave Generator",
|
||||
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
|
||||
cssClass: "icon-generator-telemetry",
|
||||
creatable: true,
|
||||
form: [
|
||||
{
|
||||
name: "Period",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "period",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"period"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Amplitude",
|
||||
control: "numberfield",
|
||||
cssClass: "l-numeric",
|
||||
key: "amplitude",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"amplitude"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Offset",
|
||||
control: "numberfield",
|
||||
cssClass: "l-numeric",
|
||||
key: "offset",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"offset"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Data Rate (hz)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "dataRateInHz",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"dataRateInHz"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Phase (radians)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "phase",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"phase"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Randomness",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "randomness",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"randomness"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Loading Delay (ms)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "loadDelay",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"loadDelay"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Include Infinity Values",
|
||||
control: "toggleSwitch",
|
||||
cssClass: "l-input",
|
||||
key: "infinityValues",
|
||||
property: [
|
||||
"telemetry",
|
||||
"infinityValues"
|
||||
]
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
period: 10,
|
||||
amplitude: 1,
|
||||
offset: 0,
|
||||
dataRateInHz: 1,
|
||||
phase: 0,
|
||||
randomness: 0,
|
||||
loadDelay: 0,
|
||||
infinityValues: false
|
||||
};
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
period: 10,
|
||||
amplitude: 1,
|
||||
offset: 0,
|
||||
dataRateInHz: 1,
|
||||
phase: 0,
|
||||
randomness: 0,
|
||||
loadDelay: 0,
|
||||
infinityValues: false,
|
||||
staleness: false
|
||||
};
|
||||
}
|
||||
});
|
||||
const stalenessProvider = new SinewaveStalenessProvider(openmct);
|
||||
});
|
||||
|
||||
openmct.telemetry.addProvider(new GeneratorProvider(openmct, stalenessProvider));
|
||||
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
|
||||
openmct.telemetry.addProvider(new SinewaveLimitProvider());
|
||||
openmct.telemetry.addProvider(stalenessProvider);
|
||||
}
|
||||
openmct.telemetry.addProvider(new GeneratorProvider(openmct));
|
||||
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
|
||||
openmct.telemetry.addProvider(new SinewaveLimitProvider());
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@@ -107,15 +107,6 @@ export default function () {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Image Thumbnail',
|
||||
key: 'thumbnail-url',
|
||||
format: 'thumbnail',
|
||||
hints: {
|
||||
thumbnail: 1
|
||||
},
|
||||
source: 'url'
|
||||
},
|
||||
{
|
||||
name: 'Image Download Name',
|
||||
key: 'imageDownloadName',
|
||||
@@ -152,16 +143,6 @@ export default function () {
|
||||
]
|
||||
});
|
||||
|
||||
const formatThumbnail = {
|
||||
format: function (url) {
|
||||
return `${url}?w=100&h=100`;
|
||||
}
|
||||
};
|
||||
|
||||
openmct.telemetry.addFormat({
|
||||
key: 'thumbnail',
|
||||
...formatThumbnail
|
||||
});
|
||||
openmct.telemetry.addProvider(getRealtimeProvider());
|
||||
openmct.telemetry.addProvider(getHistoricalProvider());
|
||||
openmct.telemetry.addProvider(getLadProvider());
|
||||
@@ -261,13 +242,6 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
|
||||
const url = imageSamples[Math.floor(timestamp / delay) % imageSamples.length];
|
||||
const urlItems = url.split('/');
|
||||
const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`;
|
||||
const navCamTransformations = {
|
||||
"translateX": 0,
|
||||
"translateY": 18,
|
||||
"rotation": 0,
|
||||
"scale": 0.3,
|
||||
"cameraAngleOfView": 70
|
||||
};
|
||||
|
||||
return {
|
||||
name,
|
||||
@@ -277,7 +251,6 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
|
||||
sunOrientation: getCompassValues(0, 360),
|
||||
cameraPan: getCompassValues(0, 360),
|
||||
heading: getCompassValues(0, 360),
|
||||
transformations: navCamTransformations,
|
||||
imageDownloadName
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
37
package.json
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.6-SNAPSHOT",
|
||||
"version": "2.1.5-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",
|
||||
"@types/jasmine": "4.3.1",
|
||||
"@types/lodash": "4.14.191",
|
||||
"babel-loader": "9.1.0",
|
||||
@@ -16,14 +15,14 @@
|
||||
"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.30.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.12.0",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"eslint-plugin-playwright": "0.11.2",
|
||||
"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,9 +38,8 @@
|
||||
"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",
|
||||
"kdbush": "^3.0.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.7.2",
|
||||
@@ -51,13 +49,12 @@
|
||||
"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",
|
||||
"plotly.js-basic-dist": "2.14.0",
|
||||
"plotly.js-gl2d-dist": "2.14.0",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.8.1",
|
||||
"sass": "1.57.1",
|
||||
"sass-loader": "13.2.0",
|
||||
"sass": "1.56.1",
|
||||
"sass-loader": "13.0.2",
|
||||
"sinon": "15.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "4.9.4",
|
||||
@@ -74,14 +71,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",
|
||||
|
||||
@@ -256,15 +256,6 @@ define([
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MCT's annotation API that enables
|
||||
* human-created comments and categorization linked to data products
|
||||
* @type {module:openmct.AnnotationAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name annotation
|
||||
*/
|
||||
this.annotation = new api.AnnotationAPI(this);
|
||||
|
||||
// Plugins that are installed by default
|
||||
this.install(this.plugins.Plot());
|
||||
this.install(this.plugins.TelemetryTable.default());
|
||||
|
||||
@@ -52,29 +52,6 @@ const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
|
||||
* @property {String} foregroundColor eg. "#ffffff"
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||
*/
|
||||
|
||||
/**
|
||||
* An interface for interacting with annotations of domain objects.
|
||||
* An annotation of a domain object is an operator created object for the purposes
|
||||
* of further describing data in plots, notebooks, maps, etc. For example, an annotation
|
||||
* could be a tag on a plot notating an interesting set of points labeled SCIENCE. It could
|
||||
* also be set of notebook entries the operator has tagged DRIVING when a robot monitored by OpenMCT
|
||||
* about rationals behind why the robot has taken a certain path.
|
||||
* Annotations are discoverable using search, and are typically rendered in OpenMCT views to bring attention
|
||||
* to other users.
|
||||
* @constructor
|
||||
*/
|
||||
export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
@@ -104,26 +81,24 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an annotation on a given domain object (e.g., a plot) and a set of targets (e.g., telemetry objects)
|
||||
* Create the a generic annotation
|
||||
* @typedef {Object} CreateAnnotationOptions
|
||||
* @property {String} name a name for the new annotation (e.g., "Plot annnotation")
|
||||
* @property {DomainObject} domainObject the domain object this annotation was created with
|
||||
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)
|
||||
* @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations
|
||||
* @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science")
|
||||
* @property {Object<string, Object>} targets The targets ID keystrings and their specific properties.
|
||||
* For plots, this will be a bounding box, e.g.: {maxY: 100, minY: 0, maxX: 100, minX: 0}
|
||||
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
|
||||
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
|
||||
* @property {String} name a name for the new parameter
|
||||
* @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create
|
||||
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create
|
||||
* @property {Tag[]} tags
|
||||
* @property {String} contentText
|
||||
* @property {import('../objects/ObjectAPI').Identifier[]} targets
|
||||
*/
|
||||
/**
|
||||
* @method create
|
||||
* @param {CreateAnnotationOptions} options
|
||||
* @returns {Promise<DomainObject>} a promise which will resolve when the domain object
|
||||
* @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object
|
||||
* has been created, or be rejected if it cannot be saved
|
||||
*/
|
||||
async create({name, domainObject, annotationType, tags, contentText, targets, targetDomainObjects}) {
|
||||
async create({name, domainObject, annotationType, tags, contentText, targets}) {
|
||||
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
|
||||
throw new Error(`Unknown annotation type: ${annotationType}`);
|
||||
}
|
||||
@@ -132,10 +107,6 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
throw new Error(`At least one target is required to create an annotation`);
|
||||
}
|
||||
|
||||
if (!Object.keys(targetDomainObjects).length) {
|
||||
throw new Error(`At least one targetDomainObject is required to create an annotation`);
|
||||
}
|
||||
|
||||
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
|
||||
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
|
||||
@@ -168,9 +139,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const success = await this.openmct.objects.save(createdObject);
|
||||
if (success) {
|
||||
this.emit('annotationCreated', createdObject);
|
||||
Object.values(targetDomainObjects).forEach(targetDomainObject => {
|
||||
this.#updateAnnotationModified(targetDomainObject);
|
||||
});
|
||||
this.#updateAnnotationModified(domainObject);
|
||||
|
||||
return createdObject;
|
||||
} else {
|
||||
@@ -178,15 +147,8 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
#updateAnnotationModified(targetDomainObject) {
|
||||
// As certain telemetry objects are immutable, we'll need to check here first
|
||||
// to see if we can add the annotation last created property.
|
||||
// TODO: This should be removed once we have a better way to handle immutable telemetry objects
|
||||
if (targetDomainObject.isMutable) {
|
||||
this.openmct.objects.mutate(targetDomainObject, this.ANNOTATION_LAST_CREATED, Date.now());
|
||||
} else {
|
||||
this.emit('targetDomainObjectAnnotated', targetDomainObject);
|
||||
}
|
||||
#updateAnnotationModified(domainObject) {
|
||||
this.openmct.objects.mutate(domainObject, this.ANNOTATION_LAST_CREATED, Date.now());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +162,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @method isAnnotation
|
||||
* @param {DomainObject} domainObject the domainObject in question
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} domainObject domainObject the domainObject in question
|
||||
* @returns {Boolean} Returns true if the domain object is an annotation
|
||||
*/
|
||||
isAnnotation(domainObject) {
|
||||
@@ -228,19 +190,56 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @method getAnnotations
|
||||
* @param {Identifier} domainObjectIdentifier - The domain object identifier to use to search for annotations. For example, a telemetry object identifier.
|
||||
* @returns {DomainObject[]} Returns an array of annotations that match the search query
|
||||
* @param {String} query - The keystring of the domain object to search for annotations for
|
||||
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns an array of domain objects that match the search query
|
||||
*/
|
||||
async getAnnotations(domainObjectIdentifier) {
|
||||
const keyStringQuery = this.openmct.objects.makeKeyString(domainObjectIdentifier);
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(keyStringQuery, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
|
||||
async getAnnotations(query) {
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
|
||||
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method addSingleAnnotationTag
|
||||
* @param {import('../objects/ObjectAPI').DomainObject=} existingAnnotation - An optional annotation to add the tag to. If not specified, we will create an annotation.
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} targetDomainObject - The domain object the annotation will point to.
|
||||
* @param {Object=} targetSpecificDetails - Optional object to add to the target object. E.g., for notebooks this would be an entryID
|
||||
* @param {AnnotationType} annotationType - The type of annotation this is for.
|
||||
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns the annotation that was either created or passed as an existingAnnotation
|
||||
*/
|
||||
async addSingleAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
|
||||
if (!existingAnnotation) {
|
||||
const targets = {};
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
|
||||
targets[targetKeyString] = targetSpecificDetails;
|
||||
const contentText = `${annotationType} tag`;
|
||||
const annotationCreationArguments = {
|
||||
name: contentText,
|
||||
domainObject: targetDomainObject,
|
||||
annotationType,
|
||||
tags: [tag],
|
||||
contentText,
|
||||
targets
|
||||
};
|
||||
const newAnnotation = await this.create(annotationCreationArguments);
|
||||
|
||||
return newAnnotation;
|
||||
} else {
|
||||
if (!existingAnnotation.tags.includes(tag)) {
|
||||
throw new Error(`Existing annotation did not contain tag ${tag}`);
|
||||
}
|
||||
|
||||
if (existingAnnotation._deleted) {
|
||||
this.unDeleteAnnotation(existingAnnotation);
|
||||
}
|
||||
|
||||
return existingAnnotation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @method deleteAnnotations
|
||||
* @param {DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
|
||||
* @param {import('../objects/ObjectAPI').DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
|
||||
*/
|
||||
deleteAnnotations(annotations) {
|
||||
if (!annotations) {
|
||||
@@ -256,7 +255,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @method deleteAnnotations
|
||||
* @param {DomainObject} annotation - An annotation to undelete (set _deleted to false)
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} existingAnnotation - An annotations to undelete (set _deleted to false)
|
||||
*/
|
||||
unDeleteAnnotation(annotation) {
|
||||
if (!annotation) {
|
||||
@@ -266,39 +265,6 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
this.openmct.objects.mutate(annotation, '_deleted', false);
|
||||
}
|
||||
|
||||
getTagsFromAnnotations(annotations, filterDuplicates = true) {
|
||||
if (!annotations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let tagsFromAnnotations = annotations.flatMap((annotation) => {
|
||||
if (annotation._deleted) {
|
||||
return [];
|
||||
} else {
|
||||
return annotation.tags;
|
||||
}
|
||||
});
|
||||
|
||||
if (filterDuplicates) {
|
||||
tagsFromAnnotations = tagsFromAnnotations.filter((tag, index, tagArray) => {
|
||||
return tagArray.indexOf(tag) === index;
|
||||
});
|
||||
}
|
||||
|
||||
const fullTagModels = this.#addTagMetaInformationToTags(tagsFromAnnotations);
|
||||
|
||||
return fullTagModels;
|
||||
}
|
||||
|
||||
#addTagMetaInformationToTags(tags) {
|
||||
return tags.map(tagKey => {
|
||||
const tagModel = this.availableTags[tagKey];
|
||||
tagModel.tagID = tagKey;
|
||||
|
||||
return tagModel;
|
||||
});
|
||||
}
|
||||
|
||||
#getMatchingTags(query) {
|
||||
if (!query) {
|
||||
return [];
|
||||
@@ -317,7 +283,12 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
#addTagMetaInformationToResults(results, matchingTagKeys) {
|
||||
const tagsAddedToResults = results.map(result => {
|
||||
const fullTagModels = this.#addTagMetaInformationToTags(result.tags);
|
||||
const fullTagModels = result.tags.map(tagKey => {
|
||||
const tagModel = this.availableTags[tagKey];
|
||||
tagModel.tagID = tagKey;
|
||||
|
||||
return tagModel;
|
||||
});
|
||||
|
||||
return {
|
||||
fullTagModels,
|
||||
@@ -367,33 +338,6 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
return combinedResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method #breakApartSeparateTargets
|
||||
* @param {Array} results A set of search results that could have the multiple targets for the same result
|
||||
* @returns {Array} The same set of results, but with each target separated out into its own result
|
||||
*/
|
||||
#breakApartSeparateTargets(results) {
|
||||
const separateResults = [];
|
||||
results.forEach(result => {
|
||||
Object.keys(result.targets).forEach(targetID => {
|
||||
const separatedResult = {
|
||||
...result
|
||||
};
|
||||
separatedResult.targets = {
|
||||
[targetID]: result.targets[targetID]
|
||||
};
|
||||
separatedResult.targetModels = result.targetModels.filter(targetModel => {
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||
|
||||
return targetKeyString === targetID;
|
||||
});
|
||||
separateResults.push(separatedResult);
|
||||
});
|
||||
});
|
||||
|
||||
return separateResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method searchForTags
|
||||
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
|
||||
@@ -416,8 +360,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const resultsWithValidPath = appliedTargetsModels.filter(result => {
|
||||
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
|
||||
});
|
||||
const breakApartSeparateTargets = this.#breakApartSeparateTargets(resultsWithValidPath);
|
||||
|
||||
return breakApartSeparateTargets;
|
||||
return resultsWithValidPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,6 @@ describe("The Annotation API", () => {
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
tags: ['sometag'],
|
||||
contentText: "fooContext",
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: {'fooTarget': {}}
|
||||
};
|
||||
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||
@@ -125,39 +124,27 @@ describe("The Annotation API", () => {
|
||||
});
|
||||
|
||||
describe("Tagging", () => {
|
||||
let tagCreationArguments;
|
||||
beforeEach(() => {
|
||||
tagCreationArguments = {
|
||||
name: 'Test Annotation',
|
||||
domainObject: mockDomainObject,
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
tags: ['aWonderfulTag'],
|
||||
contentText: 'fooContext',
|
||||
targets: {'fooNameSpace:some-object': {entryId: 'fooBarEntry'}},
|
||||
targetDomainObjects: [mockDomainObject]
|
||||
};
|
||||
});
|
||||
it("can create a tag", async () => {
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
});
|
||||
it("can delete a tag", async () => {
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
});
|
||||
it("throws an error if deleting non-existent tag", async () => {
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(() => {
|
||||
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
|
||||
}).toThrow();
|
||||
});
|
||||
it("can remove all tags", async () => {
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(() => {
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
@@ -165,13 +152,13 @@ describe("The Annotation API", () => {
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
});
|
||||
it("can add/delete/add a tag", async () => {
|
||||
let annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
let annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
@@ -61,10 +60,6 @@ export default class CompositionCollection {
|
||||
#publicAPI;
|
||||
#listeners;
|
||||
#mutables;
|
||||
#onGlobalAdd;
|
||||
#onGlobalRemove;
|
||||
static #globalEvents = new EventEmitter();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
@@ -100,21 +95,6 @@ export default class CompositionCollection {
|
||||
unobserve();
|
||||
});
|
||||
}
|
||||
|
||||
const keyString = publicAPI.objects.makeKeyString(domainObject.identifier);
|
||||
this.#onGlobalAdd = this._onGlobalAdd.bind(this);
|
||||
this.#onGlobalRemove = this._onGlobalRemove.bind(this);
|
||||
|
||||
CompositionCollection.#globalEvents.on(`add:${keyString}`, this.#onGlobalAdd);
|
||||
CompositionCollection.#globalEvents.on(`remove:${keyString}`, this.#onGlobalRemove);
|
||||
}
|
||||
|
||||
_onGlobalAdd(object) {
|
||||
this.#emit('add', object);
|
||||
}
|
||||
|
||||
_onGlobalRemove(identifier) {
|
||||
this.#emit('remove', identifier);
|
||||
}
|
||||
/**
|
||||
* Listen for changes to this composition. Supports 'add', 'remove', and
|
||||
@@ -229,21 +209,23 @@ export default class CompositionCollection {
|
||||
* **Intended for internal use ONLY.**
|
||||
* true if the underlying provider should not be updated.
|
||||
*/
|
||||
add(child) {
|
||||
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
|
||||
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
|
||||
add(child, skipMutate) {
|
||||
if (!skipMutate) {
|
||||
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
|
||||
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
|
||||
}
|
||||
|
||||
this.#provider.add(this.domainObject, child.identifier);
|
||||
} else {
|
||||
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
|
||||
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
|
||||
|
||||
child = this.#publicAPI.objects.toMutable(child);
|
||||
this.#mutables[keyString] = child;
|
||||
}
|
||||
|
||||
this.#emit('add', child);
|
||||
}
|
||||
|
||||
this.#provider.add(this.domainObject, child.identifier);
|
||||
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
|
||||
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
|
||||
|
||||
child = this.#publicAPI.objects.toMutable(child);
|
||||
this.#mutables[keyString] = child;
|
||||
}
|
||||
|
||||
// const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
|
||||
// CompositionCollection.#globalEvents.emit(`add:${keyString}`, child);
|
||||
}
|
||||
/**
|
||||
* Load the domain objects in this composition.
|
||||
@@ -258,12 +240,7 @@ export default class CompositionCollection {
|
||||
this.#cleanUpMutables();
|
||||
const children = await this.#provider.load(this.domainObject);
|
||||
const childObjects = await Promise.all(children.map((c) => this.#publicAPI.objects.get(c, abortSignal)));
|
||||
childObjects.forEach(c => {
|
||||
this.add(c);
|
||||
|
||||
const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
|
||||
CompositionCollection.#globalEvents.emit(`add:${keyString}`, c);
|
||||
});
|
||||
childObjects.forEach(c => this.add(c, true));
|
||||
this.#emit('load');
|
||||
|
||||
return childObjects;
|
||||
@@ -282,18 +259,20 @@ export default class CompositionCollection {
|
||||
* true if the underlying provider should not be updated.
|
||||
* @name remove
|
||||
*/
|
||||
remove(child) {
|
||||
this.#provider.remove(this.domainObject, child.identifier);
|
||||
if (this.returnMutables) {
|
||||
let keyString = this.#publicAPI.objects.makeKeyString(child);
|
||||
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
|
||||
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
|
||||
delete this.#mutables[keyString];
|
||||
remove(child, skipMutate) {
|
||||
if (!skipMutate) {
|
||||
this.#provider.remove(this.domainObject, child.identifier);
|
||||
} else {
|
||||
if (this.returnMutables) {
|
||||
let keyString = this.#publicAPI.objects.makeKeyString(child);
|
||||
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
|
||||
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
|
||||
delete this.#mutables[keyString];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
|
||||
// CompositionCollection.#globalEvents.emit(`remove:${keyString}`, child.identifier);
|
||||
this.#emit('remove', child);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Reorder the domain objects in this composition.
|
||||
@@ -316,10 +295,6 @@ export default class CompositionCollection {
|
||||
this.mutationListener();
|
||||
delete this.mutationListener;
|
||||
}
|
||||
|
||||
const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
|
||||
CompositionCollection.#globalEvents.off(`add:${keyString}`, this.#onGlobalAdd);
|
||||
CompositionCollection.#globalEvents.off(`remove:${keyString}`, this.#onGlobalRemove);
|
||||
}
|
||||
/**
|
||||
* Handle reorder from provider.
|
||||
|
||||
@@ -71,6 +71,10 @@ export default class CompositionProvider {
|
||||
return this.#listeningTo;
|
||||
}
|
||||
|
||||
get establishTopicListener() {
|
||||
return this.#establishTopicListener.bind(this);
|
||||
}
|
||||
|
||||
get publicAPI() {
|
||||
return this.#publicAPI;
|
||||
}
|
||||
@@ -177,6 +181,22 @@ export default class CompositionProvider {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens on general mutation topic, using injector to fetch to avoid
|
||||
* circular dependencies.
|
||||
* @private
|
||||
*/
|
||||
#establishTopicListener() {
|
||||
if (this.topicListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
|
||||
this.topicListener = () => {
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {DomainObject} parent
|
||||
@@ -196,5 +216,47 @@ export default class CompositionProvider {
|
||||
#supportsComposition(parent, _child) {
|
||||
return this.#publicAPI.composition.supportsComposition(parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mutation events. If there are active listeners for the mutated
|
||||
* object, detects changes to composition and triggers necessary events.
|
||||
*
|
||||
* @private
|
||||
* @param {DomainObject} oldDomainObject
|
||||
*/
|
||||
#onMutation(oldDomainObject) {
|
||||
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
|
||||
const listeners = this.#listeningTo[id];
|
||||
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
|
||||
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
|
||||
|
||||
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
|
||||
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
|
||||
|
||||
function notify(value) {
|
||||
return function (listener) {
|
||||
if (listener.context) {
|
||||
listener.callback.call(listener.context, value);
|
||||
} else {
|
||||
listener.callback(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
listeners.composition = newComposition.map(objectUtils.parseKeyString);
|
||||
|
||||
added.forEach(function (addedChild) {
|
||||
listeners.add.forEach(notify(addedChild));
|
||||
});
|
||||
|
||||
removed.forEach(function (removedChild) {
|
||||
listeners.remove.forEach(notify(removedChild));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
event,
|
||||
callback,
|
||||
context) {
|
||||
//this.establishTopicListener();
|
||||
this.establishTopicListener();
|
||||
|
||||
/** @type {string} */
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
@@ -157,8 +157,6 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
});
|
||||
|
||||
this.publicAPI.objects.mutate(domainObject, 'composition', composition);
|
||||
|
||||
this.objectListeners.remove?.forEach(listener => listener.callback.apply(listener.context, childId));
|
||||
}
|
||||
/**
|
||||
* Add a domain object to another domain object's composition.
|
||||
@@ -176,8 +174,6 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
if (!this.includes(parent, childId)) {
|
||||
parent.composition.push(childId);
|
||||
this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
|
||||
|
||||
this.objectListeners.add?.forEach(listener => listener.callback.apply(listener.context, childId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
<template>
|
||||
<mct-tree
|
||||
id="locator-tree"
|
||||
:is-selector-tree="true"
|
||||
:initial-selection="model.parent"
|
||||
@tree-item-selection="handleItemSelection"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -189,36 +189,34 @@ export default class ObjectAPI {
|
||||
/**
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {string} key the key for the domain object to load
|
||||
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
|
||||
* @param {boolean} [forceRemote=false] defaults to false. If true, will skip cached and
|
||||
* dirty/in-transaction objects use and the provider.get method
|
||||
* @returns {Promise<DomainObject>} a promise which will resolve when the domain object
|
||||
* @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, 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);
|
||||
}
|
||||
}
|
||||
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
if (!provider) {
|
||||
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}}"`);
|
||||
throw new Error('No Provider Matched');
|
||||
}
|
||||
|
||||
if (!provider.get) {
|
||||
@@ -393,6 +391,7 @@ export default class ObjectAPI {
|
||||
lastPersistedTime = domainObject.persisted;
|
||||
const persistedTime = Date.now();
|
||||
this.#mutate(domainObject, 'persisted', persistedTime);
|
||||
|
||||
savedObjectPromise = provider.update(domainObject);
|
||||
}
|
||||
|
||||
@@ -400,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,12 +412,11 @@ export default class ObjectAPI {
|
||||
|
||||
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)}`);
|
||||
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
|
||||
|
||||
// Synchronized objects will resolve their own conflicts, so
|
||||
// bypass the refresh here and throw the error.
|
||||
if (!this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
|
||||
if (this.isTransactionActive()) {
|
||||
this.endTransaction();
|
||||
}
|
||||
@@ -738,46 +736,6 @@ export default class ObjectAPI {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and construct an `objectPath` from a `navigationPath`.
|
||||
*
|
||||
* A `navigationPath` is a string of the form `"/browse/<keyString>/<keyString>/..."` that is used
|
||||
* by the Open MCT router to navigate to a specific object.
|
||||
*
|
||||
* Throws an error if the `navigationPath` is malformed.
|
||||
*
|
||||
* @param {string} navigationPath
|
||||
* @returns {DomainObject[]} objectPath
|
||||
*/
|
||||
async getRelativeObjectPath(navigationPath) {
|
||||
if (!navigationPath.startsWith('/browse/')) {
|
||||
throw new Error(`Malformed navigation path: "${navigationPath}"`);
|
||||
}
|
||||
|
||||
navigationPath = navigationPath.replace('/browse/', '');
|
||||
|
||||
if (!navigationPath || navigationPath === '/') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Remove any query params and split on '/'
|
||||
const keyStrings = navigationPath.split('?')?.[0].split('/');
|
||||
|
||||
if (keyStrings[0] !== 'ROOT') {
|
||||
keyStrings.unshift('ROOT');
|
||||
}
|
||||
|
||||
const objectPath = (await Promise.all(
|
||||
keyStrings.map(
|
||||
keyString => this.supportsMutation(keyString)
|
||||
? this.getMutable(utils.parseKeyString(keyString))
|
||||
: this.get(utils.parseKeyString(keyString))
|
||||
)
|
||||
)).reverse();
|
||||
|
||||
return objectPath;
|
||||
}
|
||||
|
||||
isObjectPathToALink(domainObject, objectPath) {
|
||||
return objectPath !== undefined
|
||||
&& objectPath.length > 1
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -36,7 +36,6 @@ export default class TelemetryAPI {
|
||||
this.formatMapCache = new WeakMap();
|
||||
this.formatters = new Map();
|
||||
this.limitProviders = [];
|
||||
this.stalenessProviders = [];
|
||||
this.metadataCache = new WeakMap();
|
||||
this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];
|
||||
this.noRequestProviderForAllObjects = false;
|
||||
@@ -115,10 +114,6 @@ export default class TelemetryAPI {
|
||||
if (provider.supportsLimits) {
|
||||
this.limitProviders.unshift(provider);
|
||||
}
|
||||
|
||||
if (provider.supportsStaleness) {
|
||||
this.stalenessProviders.unshift(provider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,7 +125,7 @@ export default class TelemetryAPI {
|
||||
return provider.supportsSubscribe.apply(provider, args);
|
||||
}
|
||||
|
||||
return this.subscriptionProviders.find(supportsDomainObject);
|
||||
return this.subscriptionProviders.filter(supportsDomainObject)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,25 +138,25 @@ export default class TelemetryAPI {
|
||||
return provider.supportsRequest.apply(provider, args);
|
||||
}
|
||||
|
||||
return this.requestProviders.find(supportsDomainObject);
|
||||
return this.requestProviders.filter(supportsDomainObject)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findMetadataProvider(domainObject) {
|
||||
return this.metadataProviders.find((provider) => {
|
||||
return provider.supportsMetadata(domainObject);
|
||||
});
|
||||
return this.metadataProviders.filter(function (p) {
|
||||
return p.supportsMetadata(domainObject);
|
||||
})[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findLimitEvaluator(domainObject) {
|
||||
return this.limitProviders.find((provider) => {
|
||||
return provider.supportsLimits(domainObject);
|
||||
});
|
||||
return this.limitProviders.filter(function (p) {
|
||||
return p.supportsLimits(domainObject);
|
||||
})[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,101 +351,6 @@ export default class TelemetryAPI {
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to staleness updates for a specific domain object.
|
||||
* The callback will be called whenever staleness changes.
|
||||
*
|
||||
* @method subscribeToStaleness
|
||||
* @memberof module:openmct.TelemetryAPI~StalenessProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the object
|
||||
* to watch for staleness updates
|
||||
* @param {Function} callback the callback to invoke with staleness data,
|
||||
* as it is received: ex.
|
||||
* {
|
||||
* isStale: <Boolean>,
|
||||
* timestamp: <timestamp>
|
||||
* }
|
||||
* @returns {Function} a function which may be called to terminate
|
||||
* the subscription to staleness updates
|
||||
*/
|
||||
subscribeToStaleness(domainObject, callback) {
|
||||
const provider = this.#findStalenessProvider(domainObject);
|
||||
|
||||
if (!this.stalenessSubscriberCache) {
|
||||
this.stalenessSubscriberCache = {};
|
||||
}
|
||||
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let stalenessSubscriber = this.stalenessSubscriberCache[keyString];
|
||||
|
||||
if (!stalenessSubscriber) {
|
||||
stalenessSubscriber = this.stalenessSubscriberCache[keyString] = {
|
||||
callbacks: [callback]
|
||||
};
|
||||
if (provider) {
|
||||
stalenessSubscriber.unsubscribe = provider
|
||||
.subscribeToStaleness(domainObject, (stalenessResponse) => {
|
||||
stalenessSubscriber.callbacks.forEach((cb) => {
|
||||
cb(stalenessResponse);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
stalenessSubscriber.unsubscribe = () => {};
|
||||
}
|
||||
} else {
|
||||
stalenessSubscriber.callbacks.push(callback);
|
||||
}
|
||||
|
||||
return function unsubscribe() {
|
||||
stalenessSubscriber.callbacks = stalenessSubscriber.callbacks.filter((cb) => {
|
||||
return cb !== callback;
|
||||
});
|
||||
if (stalenessSubscriber.callbacks.length === 0) {
|
||||
stalenessSubscriber.unsubscribe();
|
||||
delete this.stalenessSubscriberCache[keyString];
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request telemetry staleness for a domain object.
|
||||
*
|
||||
* @method isStale
|
||||
* @memberof module:openmct.TelemetryAPI~StalenessProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the object
|
||||
* which has associated telemetry staleness
|
||||
* @returns {Promise.<StalenessResponseObject>} a promise for a StalenessResponseObject
|
||||
* or undefined if no provider exists
|
||||
*/
|
||||
async isStale(domainObject) {
|
||||
const provider = this.#findStalenessProvider(domainObject);
|
||||
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
const options = { signal: abortController.signal };
|
||||
this.requestAbortControllers.add(abortController);
|
||||
|
||||
try {
|
||||
const staleness = await provider.isStale(domainObject, options);
|
||||
|
||||
return staleness;
|
||||
} finally {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findStalenessProvider(domainObject) {
|
||||
return this.stalenessProviders.find((provider) => {
|
||||
return provider.supportsStaleness(domainObject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get telemetry metadata for a given domain object. Returns a telemetry
|
||||
* metadata manager which provides methods for interrogating telemetry
|
||||
@@ -761,29 +661,6 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides telemetry staleness data. To subscribe to telemetry stalenes,
|
||||
* new StalenessProvider implementations should be
|
||||
* [registered]{@link module:openmct.TelemetryAPI#addProvider}.
|
||||
*
|
||||
* @interface StalenessProvider
|
||||
* @property {function} supportsStaleness receieves a domainObject and
|
||||
* returns a boolean to indicate it will provide staleness
|
||||
* @property {function} subscribeToStaleness receieves a domainObject to
|
||||
* be subscribed to and a callback to invoke with a StalenessResponseObject
|
||||
* @property {function} isStale an asynchronous method called with a domainObject
|
||||
* and an options object which currently has an abort signal, ex.
|
||||
* { signal: <AbortController.signal> }
|
||||
* this method should return a current StalenessResponseObject
|
||||
* @memberof module:openmct.TelemetryAPI~
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} StalenessResponseObject
|
||||
* @property {Boolean} isStale boolean representing the staleness state
|
||||
* @property {Number} timestamp Unix timestamp in milliseconds
|
||||
*/
|
||||
|
||||
/**
|
||||
* An interface for retrieving telemetry data associated with a domain
|
||||
* object.
|
||||
|
||||
@@ -291,6 +291,5 @@ export default class StatusAPI extends EventEmitter {
|
||||
* The Status type
|
||||
* @typedef {Object} Status
|
||||
* @property {String} key - A unique identifier for this status
|
||||
* @property {String} label - A human readable label for this status
|
||||
* @property {Number} timestamp - The time that the status was set.
|
||||
* @property {Number} label - A human readable label for this status
|
||||
*/
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<td class="js-second-data">{{ formattedTimestamp }}</td>
|
||||
<td
|
||||
class="js-third-data"
|
||||
:class="valueClasses"
|
||||
:class="valueClass"
|
||||
>{{ value }}</td>
|
||||
<td
|
||||
v-if="hasUnits"
|
||||
@@ -63,12 +63,6 @@ export default {
|
||||
hasUnits: {
|
||||
type: Boolean,
|
||||
requred: true
|
||||
},
|
||||
isStale: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -87,22 +81,14 @@ export default {
|
||||
|
||||
return this.formats[this.valueKey].format(this.datum);
|
||||
},
|
||||
valueClasses() {
|
||||
let classes = [];
|
||||
|
||||
if (this.isStale) {
|
||||
classes.push('is-stale');
|
||||
valueClass() {
|
||||
if (!this.datum) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this.datum) {
|
||||
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
|
||||
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
|
||||
|
||||
if (limit) {
|
||||
classes.push(limit.cssClass);
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
return limit ? limit.cssClass : '';
|
||||
|
||||
},
|
||||
formattedTimestamp() {
|
||||
|
||||
@@ -21,10 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-lad-table-wrapper u-style-receiver js-style-receiver"
|
||||
:class="staleClass"
|
||||
>
|
||||
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver">
|
||||
<table class="c-table c-lad-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -41,7 +38,6 @@
|
||||
:domain-object="ladRow.domainObject"
|
||||
:path-to-table="objectPath"
|
||||
:has-units="hasUnits"
|
||||
:is-stale="staleObjects.includes(ladRow.key)"
|
||||
@rowContextClick="updateViewContext"
|
||||
/>
|
||||
</tbody>
|
||||
@@ -50,9 +46,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import LadRow from './LADRow.vue';
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -72,8 +66,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
viewContext: {},
|
||||
staleObjects: []
|
||||
viewContext: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -87,13 +80,6 @@ export default {
|
||||
});
|
||||
|
||||
return itemsWithUnits.length !== 0;
|
||||
},
|
||||
staleClass() {
|
||||
if (this.staleObjects.length !== 0) {
|
||||
return 'is-stale';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -102,17 +88,11 @@ export default {
|
||||
this.composition.on('remove', this.removeItem);
|
||||
this.composition.on('reorder', this.reorder);
|
||||
this.composition.load();
|
||||
this.stalenessSubscription = {};
|
||||
},
|
||||
destroyed() {
|
||||
this.composition.off('add', this.addItem);
|
||||
this.composition.off('remove', this.removeItem);
|
||||
this.composition.off('reorder', this.reorder);
|
||||
|
||||
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
|
||||
stalenessSubscription.unsubscribe();
|
||||
stalenessSubscription.stalenessUtils.destroy();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
addItem(domainObject) {
|
||||
@@ -121,55 +101,23 @@ export default {
|
||||
item.key = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
this.items.push(item);
|
||||
|
||||
this.stalenessSubscription[item.key] = {};
|
||||
this.stalenessSubscription[item.key].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
|
||||
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
|
||||
if (stalenessResponse !== undefined) {
|
||||
this.handleStaleness(item.key, stalenessResponse);
|
||||
}
|
||||
});
|
||||
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
|
||||
this.handleStaleness(item.key, stalenessResponse);
|
||||
});
|
||||
|
||||
this.stalenessSubscription[item.key].unsubscribe = stalenessSubscription;
|
||||
},
|
||||
removeItem(identifier) {
|
||||
const SKIP_CHECK = true;
|
||||
const keystring = this.openmct.objects.makeKeyString(identifier);
|
||||
const index = this.items.findIndex(item => keystring === item.key);
|
||||
let index = this.items.findIndex(item => this.openmct.objects.makeKeyString(identifier) === item.key);
|
||||
|
||||
this.items.splice(index, 1);
|
||||
|
||||
this.stalenessSubscription[keystring].unsubscribe();
|
||||
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
|
||||
},
|
||||
reorder(reorderPlan) {
|
||||
const oldItems = this.items.slice();
|
||||
let oldItems = this.items.slice();
|
||||
reorderPlan.forEach((reorderEvent) => {
|
||||
this.$set(this.items, reorderEvent.newIndex, oldItems[reorderEvent.oldIndex]);
|
||||
});
|
||||
},
|
||||
metadataHasUnits(valueMetadatas) {
|
||||
const metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
|
||||
let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
|
||||
|
||||
return metadataWithUnits.length > 0;
|
||||
},
|
||||
handleStaleness(id, stalenessResponse, skipCheck = false) {
|
||||
if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
|
||||
const index = this.staleObjects.indexOf(id);
|
||||
if (stalenessResponse.isStale) {
|
||||
if (index === -1) {
|
||||
this.staleObjects.push(id);
|
||||
}
|
||||
} else {
|
||||
if (index !== -1) {
|
||||
this.staleObjects.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateViewContext(rowContext) {
|
||||
this.viewContext.row = rowContext;
|
||||
},
|
||||
|
||||
@@ -21,50 +21,42 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-lad-table-wrapper u-style-receiver js-style-receiver"
|
||||
:class="staleClass"
|
||||
>
|
||||
<table class="c-table c-lad-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Value</th>
|
||||
<th v-if="hasUnits">Unit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template
|
||||
v-for="ladTable in ladTableObjects"
|
||||
<table class="c-table c-lad-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Value</th>
|
||||
<th v-if="hasUnits">Unit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template
|
||||
v-for="ladTable in ladTableObjects"
|
||||
>
|
||||
<tr
|
||||
:key="ladTable.key"
|
||||
class="c-table__group-header js-lad-table-set__table-headers"
|
||||
>
|
||||
<tr
|
||||
:key="ladTable.key"
|
||||
class="c-table__group-header js-lad-table-set__table-headers"
|
||||
>
|
||||
<td colspan="10">
|
||||
{{ ladTable.domainObject.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<lad-row
|
||||
v-for="ladRow in ladTelemetryObjects[ladTable.key]"
|
||||
:key="ladRow.key"
|
||||
:domain-object="ladRow.domainObject"
|
||||
:path-to-table="ladTable.objectPath"
|
||||
:has-units="hasUnits"
|
||||
:is-stale="staleObjects.includes(ladRow.key)"
|
||||
@rowContextClick="updateViewContext"
|
||||
/>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<td colspan="10">
|
||||
{{ ladTable.domainObject.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<lad-row
|
||||
v-for="ladRow in ladTelemetryObjects[ladTable.key]"
|
||||
:key="ladRow.key"
|
||||
:domain-object="ladRow.domainObject"
|
||||
:path-to-table="ladTable.objectPath"
|
||||
:has-units="hasUnits"
|
||||
@rowContextClick="updateViewContext"
|
||||
/>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import LadRow from './LADRow.vue';
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -82,8 +74,7 @@ export default {
|
||||
ladTableObjects: [],
|
||||
ladTelemetryObjects: {},
|
||||
compositions: [],
|
||||
viewContext: {},
|
||||
staleObjects: []
|
||||
viewContext: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -104,13 +95,6 @@ export default {
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
staleClass() {
|
||||
if (this.staleObjects.length !== 0) {
|
||||
return 'is-stale';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -119,8 +103,6 @@ export default {
|
||||
this.composition.on('remove', this.removeLadTable);
|
||||
this.composition.on('reorder', this.reorderLadTables);
|
||||
this.composition.load();
|
||||
|
||||
this.stalenessSubscription = {};
|
||||
},
|
||||
destroyed() {
|
||||
this.composition.off('add', this.addLadTable);
|
||||
@@ -130,11 +112,6 @@ export default {
|
||||
c.composition.off('add', c.addCallback);
|
||||
c.composition.off('remove', c.removeCallback);
|
||||
});
|
||||
|
||||
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
|
||||
stalenessSubscription.unsubscribe();
|
||||
stalenessSubscription.stalenessUtils.destroy();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
addLadTable(domainObject) {
|
||||
@@ -183,57 +160,18 @@ export default {
|
||||
telemetryObjects.push(telemetryObject);
|
||||
|
||||
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
|
||||
|
||||
// if tracking already, possibly in another table, return
|
||||
if (this.stalenessSubscription[telemetryObject.key]) {
|
||||
return;
|
||||
} else {
|
||||
this.stalenessSubscription[telemetryObject.key] = {};
|
||||
this.stalenessSubscription[telemetryObject.key].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
|
||||
}
|
||||
|
||||
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
|
||||
if (stalenessResponse !== undefined) {
|
||||
this.handleStaleness(telemetryObject.key, stalenessResponse);
|
||||
}
|
||||
});
|
||||
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
|
||||
this.handleStaleness(telemetryObject.key, stalenessResponse);
|
||||
});
|
||||
|
||||
this.stalenessSubscription[telemetryObject.key].unsubscribe = stalenessSubscription;
|
||||
};
|
||||
},
|
||||
removeTelemetryObject(ladTable) {
|
||||
return (identifier) => {
|
||||
const SKIP_CHECK = true;
|
||||
const keystring = this.openmct.objects.makeKeyString(identifier);
|
||||
let telemetryObjects = this.ladTelemetryObjects[ladTable.key];
|
||||
let index = telemetryObjects.findIndex(telemetryObject => keystring === telemetryObject.key);
|
||||
let index = telemetryObjects.findIndex(telemetryObject => this.openmct.objects.makeKeyString(identifier) === telemetryObject.key);
|
||||
|
||||
telemetryObjects.splice(index, 1);
|
||||
|
||||
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
|
||||
|
||||
this.stalenessSubscription[keystring].unsubscribe();
|
||||
this.stalenessSubscription[keystring].stalenessUtils.destroy();
|
||||
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
|
||||
};
|
||||
},
|
||||
handleStaleness(id, stalenessResponse, skipCheck = false) {
|
||||
if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
|
||||
const index = this.staleObjects.indexOf(id);
|
||||
if (stalenessResponse.isStale) {
|
||||
if (index === -1) {
|
||||
this.staleObjects.push(id);
|
||||
}
|
||||
} else {
|
||||
if (index !== -1) {
|
||||
this.staleObjects.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateViewContext(rowContext) {
|
||||
this.viewContext.row = rowContext;
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ import TelemetryCriterion from "./criterion/TelemetryCriterion";
|
||||
import { evaluateResults } from './utils/evaluator';
|
||||
import { getLatestTimestamp } from './utils/time';
|
||||
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
|
||||
import { TRIGGER_CONJUNCTION, TRIGGER_LABEL } from "./utils/constants";
|
||||
import {TRIGGER_CONJUNCTION, TRIGGER_LABEL} from "./utils/constants";
|
||||
|
||||
/*
|
||||
* conditionConfiguration = {
|
||||
@@ -160,8 +160,7 @@ export default class Condition extends EventEmitter {
|
||||
}
|
||||
|
||||
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
criterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
criterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
|
||||
if (!this.criteria) {
|
||||
this.criteria = [];
|
||||
}
|
||||
@@ -192,14 +191,12 @@ export default class Condition extends EventEmitter {
|
||||
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
|
||||
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
|
||||
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
newCriterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
newCriterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
newCriterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
|
||||
|
||||
let criterion = found.item;
|
||||
criterion.unsubscribe();
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
newCriterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
|
||||
this.criteria.splice(found.index, 1, newCriterion);
|
||||
}
|
||||
}
|
||||
@@ -208,9 +205,12 @@ export default class Condition extends EventEmitter {
|
||||
let found = this.findCriterion(id);
|
||||
if (found) {
|
||||
let criterion = found.item;
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
criterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
criterion.off('criterionUpdated', (obj) => {
|
||||
this.handleCriterionUpdated(obj);
|
||||
});
|
||||
criterion.off('telemetryIsStale', (obj) => {
|
||||
this.handleStaleCriterion(obj);
|
||||
});
|
||||
criterion.destroy();
|
||||
this.criteria.splice(found.index, 1);
|
||||
|
||||
@@ -227,7 +227,7 @@ export default class Condition extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
handleOldTelemetryCriterion(updatedCriterion) {
|
||||
handleStaleCriterion(updatedCriterion) {
|
||||
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
|
||||
let latestTimestamp = {};
|
||||
latestTimestamp = getLatestTimestamp(
|
||||
@@ -239,11 +239,6 @@ export default class Condition extends EventEmitter {
|
||||
this.conditionManager.updateCurrentCondition(latestTimestamp);
|
||||
}
|
||||
|
||||
handleTelemetryStaleness() {
|
||||
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
|
||||
this.conditionManager.updateCurrentCondition();
|
||||
}
|
||||
|
||||
updateDescription() {
|
||||
const triggerDescription = this.getTriggerDescription();
|
||||
let description = '';
|
||||
|
||||
@@ -82,10 +82,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Condition from './Condition.vue';
|
||||
import ConditionManager from '../ConditionManager';
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -141,13 +139,6 @@ export default {
|
||||
if (this.stopObservingForChanges) {
|
||||
this.stopObservingForChanges();
|
||||
}
|
||||
|
||||
if (this.stalenessSubscription) {
|
||||
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
|
||||
stalenessSubscription.unsubscribe();
|
||||
stalenessSubscription.stalenessUtils.destroy();
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
@@ -159,7 +150,6 @@ export default {
|
||||
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
|
||||
this.conditionManager.on('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
|
||||
this.updateDefaultCondition();
|
||||
this.stalenessSubscription = {};
|
||||
},
|
||||
methods: {
|
||||
handleConditionSetResultUpdated(data) {
|
||||
@@ -220,57 +210,19 @@ export default {
|
||||
return arr;
|
||||
},
|
||||
addTelemetryObject(domainObject) {
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
this.telemetryObjs.push(domainObject);
|
||||
this.$emit('telemetryUpdated', this.telemetryObjs);
|
||||
|
||||
if (!this.stalenessSubscription[keyString]) {
|
||||
this.stalenessSubscription[keyString] = {};
|
||||
}
|
||||
|
||||
this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
|
||||
|
||||
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
|
||||
this.hanldeStaleness(keyString, stalenessResponse);
|
||||
});
|
||||
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
|
||||
this.hanldeStaleness(keyString, stalenessResponse);
|
||||
});
|
||||
|
||||
this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription;
|
||||
},
|
||||
removeTelemetryObject(identifier) {
|
||||
const keyString = this.openmct.objects.makeKeyString(identifier);
|
||||
const index = this.telemetryObjs.findIndex(obj => {
|
||||
let index = this.telemetryObjs.findIndex(obj => {
|
||||
let objId = this.openmct.objects.makeKeyString(obj.identifier);
|
||||
let id = this.openmct.objects.makeKeyString(identifier);
|
||||
|
||||
return objId === keyString;
|
||||
return objId === id;
|
||||
});
|
||||
|
||||
if (index > -1) {
|
||||
this.telemetryObjs.splice(index, 1);
|
||||
}
|
||||
|
||||
if (this.stalenessSubscription[keyString]) {
|
||||
this.stalenessSubscription[keyString].unsubscribe();
|
||||
this.stalenessSubscription[keyString].stalenessUtils.destroy();
|
||||
this.emitStaleness({
|
||||
keyString,
|
||||
isStale: false
|
||||
});
|
||||
}
|
||||
},
|
||||
hanldeStaleness(keyString, stalenessResponse) {
|
||||
if (this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
|
||||
this.emitStaleness({
|
||||
keyString,
|
||||
isStale: stalenessResponse.isStale
|
||||
});
|
||||
}
|
||||
},
|
||||
emitStaleness(stalenessObject) {
|
||||
this.$emit('telemetryStaleness', stalenessObject);
|
||||
},
|
||||
addCondition() {
|
||||
this.conditionManager.addCondition();
|
||||
|
||||
@@ -21,10 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-cs"
|
||||
:class="{'is-stale': isStale }"
|
||||
>
|
||||
<div class="c-cs">
|
||||
<section class="c-cs__current-output c-section">
|
||||
<div class="c-cs__content c-cs__current-output-value">
|
||||
<span class="c-cs__current-output-value__label">Current Output</span>
|
||||
@@ -53,7 +50,6 @@
|
||||
@conditionSetResultUpdated="updateCurrentOutput"
|
||||
@updateDefaultOutput="updateDefaultOutput"
|
||||
@telemetryUpdated="updateTelemetry"
|
||||
@telemetryStaleness="handleStaleness"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,15 +73,9 @@ export default {
|
||||
currentConditionOutput: '',
|
||||
defaultConditionOutput: '',
|
||||
telemetryObjs: [],
|
||||
testData: {},
|
||||
staleObjects: []
|
||||
testData: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isStale() {
|
||||
return this.staleObjects.length !== 0;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.conditionSetIdentifier = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.testData = {
|
||||
@@ -105,18 +95,6 @@ export default {
|
||||
},
|
||||
updateTestData(testData) {
|
||||
this.testData = testData;
|
||||
},
|
||||
handleStaleness({ keyString, isStale }) {
|
||||
const index = this.staleObjects.indexOf(keyString);
|
||||
if (isStale) {
|
||||
if (index === -1) {
|
||||
this.staleObjects.push(keyString);
|
||||
}
|
||||
} else {
|
||||
if (index !== -1) {
|
||||
this.staleObjects.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
>
|
||||
<span v-if="inputIndex < inputCount-1">and</span>
|
||||
</span>
|
||||
<span v-if="criterion.metadata === 'dataReceived' && criterion.operation.name === IS_OLD_KEY">seconds</span>
|
||||
<span v-if="criterion.metadata === 'dataReceived'">seconds</span>
|
||||
</template>
|
||||
<span v-else>
|
||||
<span
|
||||
@@ -122,7 +122,7 @@
|
||||
|
||||
<script>
|
||||
import { OPERATIONS, INPUT_TYPES } from '../utils/operations';
|
||||
import { TRIGGER_CONJUNCTION, IS_OLD_KEY, IS_STALE_KEY } from "../utils/constants";
|
||||
import {TRIGGER_CONJUNCTION} from "../utils/constants";
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
@@ -153,8 +153,7 @@ export default {
|
||||
rowLabel: '',
|
||||
operationFormat: '',
|
||||
enumerations: [],
|
||||
inputTypes: INPUT_TYPES,
|
||||
IS_OLD_KEY
|
||||
inputTypes: INPUT_TYPES
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -165,7 +164,7 @@ export default {
|
||||
},
|
||||
filteredOps: function () {
|
||||
if (this.criterion.metadata === 'dataReceived') {
|
||||
return this.operations.filter(op => op.name === IS_OLD_KEY || op.name === IS_STALE_KEY);
|
||||
return this.operations.filter(op => op.name === 'isStale');
|
||||
} else {
|
||||
return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1);
|
||||
}
|
||||
|
||||
@@ -54,10 +54,6 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-stale {
|
||||
@include isStaleHolder();
|
||||
}
|
||||
|
||||
/************************** CONDITION SET LAYOUT */
|
||||
&__current-output {
|
||||
flex: 0 0 auto;
|
||||
|
||||
@@ -21,9 +21,8 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import TelemetryCriterion from './TelemetryCriterion';
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
import { evaluateResults } from "../utils/evaluator";
|
||||
import { getLatestTimestamp, checkIfOld } from '../utils/time';
|
||||
import {getLatestTimestamp, subscribeForStaleness} from '../utils/time';
|
||||
import { getOperatorText } from "@/plugins/condition/utils/operations";
|
||||
|
||||
export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
@@ -39,41 +38,13 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
initialize() {
|
||||
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
|
||||
this.telemetryDataCache = {};
|
||||
|
||||
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
|
||||
this.checkForOldData(this.telemetryObjects || {});
|
||||
}
|
||||
|
||||
if (this.isValid() && this.isStalenessCheck()) {
|
||||
this.subscribeToStaleness(this.telemetryObjects || {});
|
||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||
this.subscribeForStaleData(this.telemetryObjects || {});
|
||||
}
|
||||
}
|
||||
|
||||
checkForOldData(telemetryObjects) {
|
||||
if (!this.ageCheck) {
|
||||
this.ageCheck = {};
|
||||
}
|
||||
subscribeForStaleData(telemetryObjects) {
|
||||
|
||||
Object.values(telemetryObjects).forEach((telemetryObject) => {
|
||||
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
if (!this.ageCheck[id]) {
|
||||
this.ageCheck[id] = checkIfOld((data) => {
|
||||
this.handleOldTelemetry(id, data);
|
||||
}, this.input[0] * 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleOldTelemetry(id, data) {
|
||||
if (this.telemetryDataCache) {
|
||||
this.telemetryDataCache[id] = true;
|
||||
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
|
||||
}
|
||||
|
||||
this.emitEvent('telemetryIsOld', data);
|
||||
}
|
||||
|
||||
subscribeToStaleness(telemetryObjects) {
|
||||
if (!this.stalenessSubscription) {
|
||||
this.stalenessSubscription = {};
|
||||
}
|
||||
@@ -81,27 +52,20 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
Object.values(telemetryObjects).forEach((telemetryObject) => {
|
||||
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
if (!this.stalenessSubscription[id]) {
|
||||
this.stalenessSubscription[id] = {};
|
||||
this.stalenessSubscription[id].stalenessUtils = new StalenessUtils(this.openmct, telemetryObject);
|
||||
this.stalenessSubscription[id].unsubscribe = this.openmct.telemetry.subscribeToStaleness(
|
||||
telemetryObject,
|
||||
(stalenessResponse) => {
|
||||
this.handleStaleTelemetry(id, stalenessResponse);
|
||||
}
|
||||
);
|
||||
this.stalenessSubscription[id] = subscribeForStaleness((data) => {
|
||||
this.handleStaleTelemetry(id, data);
|
||||
}, this.input[0] * 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleStaleTelemetry(id, stalenessResponse) {
|
||||
handleStaleTelemetry(id, data) {
|
||||
if (this.telemetryDataCache) {
|
||||
if (this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
|
||||
this.telemetryDataCache[id] = stalenessResponse.isStale;
|
||||
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
|
||||
|
||||
this.emitEvent('telemetryStaleness');
|
||||
}
|
||||
this.telemetryDataCache[id] = true;
|
||||
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
|
||||
}
|
||||
|
||||
this.emitEvent('telemetryIsStale', data);
|
||||
}
|
||||
|
||||
isValid() {
|
||||
@@ -111,13 +75,8 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
updateTelemetryObjects(telemetryObjects) {
|
||||
this.telemetryObjects = { ...telemetryObjects };
|
||||
this.removeTelemetryDataCache();
|
||||
|
||||
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
|
||||
this.checkForOldData(this.telemetryObjects || {});
|
||||
}
|
||||
|
||||
if (this.isValid() && this.isStalenessCheck()) {
|
||||
this.subscribeToStaleness(this.telemetryObjects || {});
|
||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||
this.subscribeForStaleData(this.telemetryObjects || {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,9 +91,6 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
});
|
||||
telemetryCacheIds.forEach(id => {
|
||||
delete (this.telemetryDataCache[id]);
|
||||
delete (this.ageCheck[id]);
|
||||
this.stalenessSubscription[id].unsubscribe();
|
||||
this.stalenessSubscription[id].stalenessUtils.destroy();
|
||||
delete (this.stalenessSubscription[id]);
|
||||
});
|
||||
}
|
||||
@@ -169,10 +125,10 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
updateResult(data, telemetryObjects) {
|
||||
const validatedData = this.isValid() ? data : {};
|
||||
|
||||
if (validatedData && !this.isStalenessCheck()) {
|
||||
if (this.isOldCheck()) {
|
||||
if (this.ageCheck?.[validatedData.id]) {
|
||||
this.ageCheck[validatedData.id].update(validatedData);
|
||||
if (validatedData) {
|
||||
if (this.isStalenessCheck()) {
|
||||
if (this.stalenessSubscription && this.stalenessSubscription[validatedData.id]) {
|
||||
this.stalenessSubscription[validatedData.id].update(validatedData);
|
||||
}
|
||||
|
||||
this.telemetryDataCache[validatedData.id] = false;
|
||||
@@ -270,17 +226,9 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
destroy() {
|
||||
delete this.telemetryObjects;
|
||||
delete this.telemetryDataCache;
|
||||
|
||||
if (this.ageCheck) {
|
||||
Object.values(this.ageCheck).forEach((subscription) => subscription.clear);
|
||||
delete this.ageCheck;
|
||||
}
|
||||
|
||||
if (this.stalenessSubscription) {
|
||||
Object.values(this.stalenessSubscription).forEach(subscription => {
|
||||
subscription.unsubscribe();
|
||||
subscription.stalenessUtils.destroy();
|
||||
});
|
||||
Object.values(this.stalenessSubscription).forEach((subscription) => subscription.clear);
|
||||
delete this.stalenessSubscription;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,8 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
import { IS_OLD_KEY, IS_STALE_KEY } from '../utils/constants';
|
||||
import { OPERATIONS, getOperatorText } from '../utils/operations';
|
||||
import { checkIfOld } from "../utils/time";
|
||||
import { subscribeForStaleness } from "../utils/time";
|
||||
|
||||
export default class TelemetryCriterion extends EventEmitter {
|
||||
|
||||
@@ -46,8 +44,7 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
this.input = telemetryDomainObjectDefinition.input;
|
||||
this.metadata = telemetryDomainObjectDefinition.metadata;
|
||||
this.result = undefined;
|
||||
this.ageCheck = undefined;
|
||||
this.unsubscribeFromStaleness = undefined;
|
||||
this.stalenessSubscription = undefined;
|
||||
|
||||
this.initialize();
|
||||
this.emitEvent('criterionUpdated', this);
|
||||
@@ -60,13 +57,8 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
}
|
||||
|
||||
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
|
||||
|
||||
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
|
||||
this.checkForOldData();
|
||||
}
|
||||
|
||||
if (this.isValid() && this.isStalenessCheck()) {
|
||||
this.subscribeToStaleness();
|
||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||
this.subscribeForStaleData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,52 +66,25 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
return this.telemetryObjectIdAsString && (this.telemetryObjectIdAsString === id);
|
||||
}
|
||||
|
||||
checkForOldData() {
|
||||
if (this.ageCheck) {
|
||||
this.ageCheck.clear();
|
||||
subscribeForStaleData() {
|
||||
if (this.stalenessSubscription) {
|
||||
this.stalenessSubscription.clear();
|
||||
}
|
||||
|
||||
this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000);
|
||||
this.stalenessSubscription = subscribeForStaleness(this.handleStaleTelemetry.bind(this), this.input[0] * 1000);
|
||||
}
|
||||
|
||||
handleOldTelemetry(data) {
|
||||
handleStaleTelemetry(data) {
|
||||
this.result = true;
|
||||
this.emitEvent('telemetryIsOld', data);
|
||||
}
|
||||
|
||||
subscribeToStaleness() {
|
||||
if (this.unsubscribeFromStaleness) {
|
||||
this.unsubscribeFromStaleness();
|
||||
}
|
||||
|
||||
if (!this.stalenessUtils) {
|
||||
this.stalenessUtils = new StalenessUtils(this.openmct, this.telemetryObject);
|
||||
}
|
||||
|
||||
this.openmct.telemetry.isStale(this.telemetryObject).then(this.handleStaleTelemetry.bind(this));
|
||||
this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness(
|
||||
this.telemetryObject,
|
||||
this.handleStaleTelemetry.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
handleStaleTelemetry(stalenessResponse) {
|
||||
if (stalenessResponse !== undefined && this.stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
|
||||
this.result = stalenessResponse.isStale;
|
||||
this.emitEvent('telemetryStaleness');
|
||||
}
|
||||
this.emitEvent('telemetryIsStale', data);
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.telemetryObject && this.metadata && this.operation;
|
||||
}
|
||||
|
||||
isOldCheck() {
|
||||
return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_OLD_KEY;
|
||||
}
|
||||
|
||||
isStalenessCheck() {
|
||||
return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_STALE_KEY;
|
||||
return this.metadata && this.metadata === 'dataReceived';
|
||||
}
|
||||
|
||||
isValidInput() {
|
||||
@@ -128,13 +93,8 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
|
||||
updateTelemetryObjects(telemetryObjects) {
|
||||
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
|
||||
|
||||
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
|
||||
this.checkForOldData();
|
||||
}
|
||||
|
||||
if (this.isValid() && this.isStalenessCheck()) {
|
||||
this.subscribeToStaleness();
|
||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||
this.subscribeForStaleData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,17 +130,14 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
|
||||
updateResult(data) {
|
||||
const validatedData = this.isValid() ? data : {};
|
||||
|
||||
if (!this.isStalenessCheck()) {
|
||||
if (this.isOldCheck()) {
|
||||
if (this.ageCheck) {
|
||||
this.ageCheck.update(validatedData);
|
||||
}
|
||||
|
||||
this.result = false;
|
||||
} else {
|
||||
this.result = this.computeResult(validatedData);
|
||||
if (this.isStalenessCheck()) {
|
||||
if (this.stalenessSubscription) {
|
||||
this.stalenessSubscription.update(validatedData);
|
||||
}
|
||||
|
||||
this.result = false;
|
||||
} else {
|
||||
this.result = this.computeResult(validatedData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,17 +268,8 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
destroy() {
|
||||
delete this.telemetryObject;
|
||||
delete this.telemetryObjectIdAsString;
|
||||
|
||||
if (this.ageCheck) {
|
||||
delete this.ageCheck;
|
||||
}
|
||||
|
||||
if (this.stalenessUtils) {
|
||||
this.stalenessUtils.destroy();
|
||||
}
|
||||
|
||||
if (this.unsubscribeFromStaleness) {
|
||||
this.unsubscribeFromStaleness();
|
||||
if (this.stalenessSubscription) {
|
||||
delete this.stalenessSubscription;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import Vue from 'vue';
|
||||
import {getApplicableStylesForItem} from "./utils/styleUtils";
|
||||
import ConditionManager from "@/plugins/condition/ConditionManager";
|
||||
import StyleRuleManager from "./StyleRuleManager";
|
||||
import { IS_OLD_KEY } from "./utils/constants";
|
||||
|
||||
describe('the plugin', function () {
|
||||
let conditionSetDefinition;
|
||||
@@ -643,7 +642,7 @@ describe('the plugin', function () {
|
||||
|
||||
});
|
||||
|
||||
describe('the condition check if old', () => {
|
||||
describe('the condition check for staleness', () => {
|
||||
let conditionSetDomainObject;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -661,13 +660,13 @@ describe('the plugin', function () {
|
||||
"id": "39584410-cbf9-499e-96dc-76f27e69885d",
|
||||
"configuration": {
|
||||
"name": "Unnamed Condition",
|
||||
"output": "Any old telemetry",
|
||||
"output": "Any stale telemetry",
|
||||
"trigger": "all",
|
||||
"criteria": [
|
||||
{
|
||||
"id": "35400132-63b0-425c-ac30-8197df7d5862",
|
||||
"telemetry": "any",
|
||||
"operation": IS_OLD_KEY,
|
||||
"operation": "isStale",
|
||||
"input": [
|
||||
"0.2"
|
||||
],
|
||||
@@ -675,7 +674,7 @@ describe('the plugin', function () {
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": "Match if all criteria are met: Any telemetry is old after 5 seconds"
|
||||
"summary": "Match if all criteria are met: Any telemetry is stale after 5 seconds"
|
||||
},
|
||||
{
|
||||
"isDefault": true,
|
||||
@@ -709,7 +708,7 @@ describe('the plugin', function () {
|
||||
};
|
||||
});
|
||||
|
||||
it('should evaluate as old when telemetry is not received in the allotted time', (done) => {
|
||||
it('should evaluate as stale when telemetry is not received in the allotted time', (done) => {
|
||||
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
||||
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
||||
conditionMgr.telemetryObjects = {
|
||||
@@ -718,7 +717,7 @@ describe('the plugin', function () {
|
||||
conditionMgr.updateConditionTelemetryObjects();
|
||||
setTimeout(() => {
|
||||
expect(mockListener).toHaveBeenCalledWith({
|
||||
output: 'Any old telemetry',
|
||||
output: 'Any stale telemetry',
|
||||
id: {
|
||||
namespace: '',
|
||||
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
|
||||
@@ -730,7 +729,7 @@ describe('the plugin', function () {
|
||||
}, 400);
|
||||
});
|
||||
|
||||
it('should not evaluate as old when telemetry is received in the allotted time', (done) => {
|
||||
it('should not evaluate as stale when telemetry is received in the allotted time', (done) => {
|
||||
const date = 1;
|
||||
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = ["0.4"];
|
||||
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
||||
|
||||
@@ -59,6 +59,3 @@ export const ERROR = {
|
||||
errorText: 'Condition not found'
|
||||
}
|
||||
};
|
||||
|
||||
export const IS_OLD_KEY = 'isStale';
|
||||
export const IS_STALE_KEY = 'isStale.new';
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { IS_OLD_KEY, IS_STALE_KEY } from "./constants";
|
||||
|
||||
function convertToNumbers(input) {
|
||||
let numberInputs = [];
|
||||
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
|
||||
@@ -297,7 +295,7 @@ export const OPERATIONS = [
|
||||
}
|
||||
},
|
||||
{
|
||||
name: IS_OLD_KEY,
|
||||
name: 'isStale',
|
||||
operation: function () {
|
||||
return false;
|
||||
},
|
||||
@@ -307,18 +305,6 @@ export const OPERATIONS = [
|
||||
getDescription: function (values) {
|
||||
return ` is older than ${values[0] || ''} seconds`;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: IS_STALE_KEY,
|
||||
operation: function () {
|
||||
return false;
|
||||
},
|
||||
text: 'is stale',
|
||||
appliesTo: ["number"],
|
||||
inputCount: 0,
|
||||
getDescription: function () {
|
||||
return ' is stale';
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -330,5 +316,5 @@ export const INPUT_TYPES = {
|
||||
export function getOperatorText(operationName, values) {
|
||||
const found = OPERATIONS.find((operation) => operation.name === operationName);
|
||||
|
||||
return found?.getDescription(values) ?? '';
|
||||
return found ? found.getDescription(values) : '';
|
||||
}
|
||||
|
||||
@@ -51,26 +51,26 @@ export function getLatestTimestamp(
|
||||
return latest;
|
||||
}
|
||||
|
||||
export function checkIfOld(callback, timeout) {
|
||||
let oldCheckTimer = setTimeout(() => {
|
||||
clearTimeout(oldCheckTimer);
|
||||
export function subscribeForStaleness(callback, timeout) {
|
||||
let stalenessTimer = setTimeout(() => {
|
||||
clearTimeout(stalenessTimer);
|
||||
callback();
|
||||
}, timeout);
|
||||
|
||||
return {
|
||||
update: (data) => {
|
||||
if (oldCheckTimer) {
|
||||
clearTimeout(oldCheckTimer);
|
||||
if (stalenessTimer) {
|
||||
clearTimeout(stalenessTimer);
|
||||
}
|
||||
|
||||
oldCheckTimer = setTimeout(() => {
|
||||
clearTimeout(oldCheckTimer);
|
||||
stalenessTimer = setTimeout(() => {
|
||||
clearTimeout(stalenessTimer);
|
||||
callback(data);
|
||||
}, timeout);
|
||||
},
|
||||
clear: () => {
|
||||
if (oldCheckTimer) {
|
||||
clearTimeout(oldCheckTimer);
|
||||
if (stalenessTimer) {
|
||||
clearTimeout(stalenessTimer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { checkIfOld } from "./time";
|
||||
import { subscribeForStaleness } from "./time";
|
||||
|
||||
describe('time related utils', () => {
|
||||
let subscription;
|
||||
@@ -27,11 +27,11 @@ describe('time related utils', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockListener = jasmine.createSpy('listener');
|
||||
subscription = checkIfOld(mockListener, 100);
|
||||
subscription = subscribeForStaleness(mockListener, 100);
|
||||
});
|
||||
|
||||
describe('check if old', () => {
|
||||
it('should call listeners when old', (done) => {
|
||||
describe('subscribe for staleness', () => {
|
||||
it('should call listeners when stale', (done) => {
|
||||
setTimeout(() => {
|
||||
expect(mockListener).toHaveBeenCalled();
|
||||
done();
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<div
|
||||
v-if="domainObject"
|
||||
class="c-telemetry-view u-style-receiver"
|
||||
:class="[itemClasses]"
|
||||
:class="[statusClass]"
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
@@ -73,7 +73,6 @@
|
||||
<script>
|
||||
import LayoutFrame from './LayoutFrame.vue';
|
||||
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
import { getDefaultNotebook, getNotebookSectionAndPage } from '@/plugins/notebook/utils/notebook-storage.js';
|
||||
|
||||
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
|
||||
@@ -103,7 +102,7 @@ export default {
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin, stalenessMixin],
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct', 'objectPath', 'currentView'],
|
||||
props: {
|
||||
item: {
|
||||
@@ -138,18 +137,8 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
itemClasses() {
|
||||
let classes = [];
|
||||
|
||||
if (this.status) {
|
||||
classes.push(`is-status--${this.status}`);
|
||||
}
|
||||
|
||||
if (this.isStale) {
|
||||
classes.push('is-stale');
|
||||
}
|
||||
|
||||
return classes;
|
||||
statusClass() {
|
||||
return (this.status) ? `is-status--${this.status}` : '';
|
||||
},
|
||||
showLabel() {
|
||||
let displayMode = this.item.displayMode;
|
||||
@@ -321,7 +310,6 @@ export default {
|
||||
this.removeSelectable = this.openmct.selection.selectable(
|
||||
this.$el, this.context, this.immediatelySelect || this.initSelect);
|
||||
delete this.immediatelySelect;
|
||||
this.subscribeToStaleness(this.domainObject);
|
||||
},
|
||||
updateTelemetryFormat(format) {
|
||||
this.customStringformatter.setFormat(format);
|
||||
|
||||
@@ -25,12 +25,6 @@
|
||||
margin-right: $interiorMargin;
|
||||
}
|
||||
|
||||
&.is-stale {
|
||||
.c-telemetry-view__value {
|
||||
@include isStaleElement();
|
||||
}
|
||||
}
|
||||
|
||||
.c-frame & {
|
||||
@include abs();
|
||||
border: 1px solid transparent;
|
||||
|
||||
@@ -31,7 +31,6 @@ export default class DuplicateAction {
|
||||
this.priority = 7;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.transaction = null;
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
@@ -46,9 +45,7 @@ export default class DuplicateAction {
|
||||
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier));
|
||||
}
|
||||
|
||||
async onSave(changes) {
|
||||
this.startTransaction();
|
||||
|
||||
onSave(changes) {
|
||||
let inNavigationPath = this.inNavigationPath();
|
||||
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
@@ -62,9 +59,7 @@ export default class DuplicateAction {
|
||||
const parentDomainObjectpath = changes.location || [this.parent];
|
||||
const parent = parentDomainObjectpath[0];
|
||||
|
||||
await duplicationTask.duplicate(this.object, parent);
|
||||
|
||||
return this.saveTransaction();
|
||||
return duplicationTask.duplicate(this.object, parent);
|
||||
}
|
||||
|
||||
showForm(domainObject, parentDomainObject) {
|
||||
@@ -147,20 +142,4 @@ export default class DuplicateAction {
|
||||
&& parentType.definition.creatable
|
||||
&& Array.isArray(parent.composition);
|
||||
}
|
||||
|
||||
startTransaction() {
|
||||
if (!this.openmct.objects.isTransactionActive()) {
|
||||
this.transaction = this.openmct.objects.startTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
async saveTransaction() {
|
||||
if (!this.transaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.transaction.commit();
|
||||
this.openmct.objects.endTransaction();
|
||||
this.transaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="c-gauge__wrapper js-gauge-wrapper"
|
||||
:class="gaugeClasses"
|
||||
:class="`c-gauge--${gaugeType}`"
|
||||
:title="gaugeTitle"
|
||||
>
|
||||
<template v-if="typeDial">
|
||||
@@ -347,14 +347,12 @@
|
||||
|
||||
<script>
|
||||
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
|
||||
const LIMIT_PADDING_IN_PERCENT = 10;
|
||||
const DEFAULT_CURRENT_VALUE = '--';
|
||||
|
||||
export default {
|
||||
name: 'Gauge',
|
||||
mixins: [stalenessMixin],
|
||||
inject: ['openmct', 'domainObject', 'composition'],
|
||||
data() {
|
||||
let gaugeController = this.domainObject.configuration.gaugeController;
|
||||
@@ -405,15 +403,6 @@ export default {
|
||||
|
||||
return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO);
|
||||
},
|
||||
gaugeClasses() {
|
||||
let classes = [`c-gauge--${this.gaugeType}`];
|
||||
|
||||
if (this.isStale) {
|
||||
classes.push('is-stale');
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
rangeFontSize() {
|
||||
const CHAR_THRESHOLD = 3;
|
||||
const START_PERC = 8.5;
|
||||
@@ -564,8 +553,6 @@ export default {
|
||||
this.telemetryObject = domainObject;
|
||||
this.request();
|
||||
this.subscribe();
|
||||
|
||||
this.subscribeToStaleness(domainObject);
|
||||
},
|
||||
addedToComposition(domainObject) {
|
||||
if (this.telemetryObject) {
|
||||
@@ -624,8 +611,6 @@ export default {
|
||||
this.unsubscribe = null;
|
||||
}
|
||||
|
||||
this.triggerUnsubscribeFromStaleness();
|
||||
|
||||
this.curVal = DEFAULT_CURRENT_VALUE;
|
||||
this.formats = null;
|
||||
this.limitHigh = '';
|
||||
|
||||
@@ -32,15 +32,6 @@ $meterNeedleBorderRadius: 5px;
|
||||
&__wrapper {
|
||||
@include abs();
|
||||
overflow: hidden;
|
||||
|
||||
&.is-stale {
|
||||
@include isStaleHolder();
|
||||
|
||||
[class*=__current-value-text] {
|
||||
fill: $colorTelemStale;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__current-value-text-wrapper {
|
||||
|
||||
@@ -45,35 +45,6 @@ export default class ImageryView {
|
||||
});
|
||||
}
|
||||
|
||||
getViewContext() {
|
||||
if (!this.component) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return this.component.$refs.ImageryContainer;
|
||||
}
|
||||
|
||||
pause() {
|
||||
const imageContext = this.getViewContext();
|
||||
// persist previous pause value to return to after unpausing
|
||||
this.previouslyPaused = imageContext.isPaused;
|
||||
imageContext.thumbnailClicked(imageContext.focusedImageIndex);
|
||||
}
|
||||
unpause() {
|
||||
const pausedStateBefore = this.previouslyPaused;
|
||||
this.previouslyPaused = undefined; // clear value
|
||||
const imageContext = this.getViewContext();
|
||||
imageContext.paused(pausedStateBefore);
|
||||
}
|
||||
|
||||
onPreviewModeChange({ isPreviewing } = {}) {
|
||||
if (isPreviewing) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.unpause();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.component.$destroy();
|
||||
this.component = undefined;
|
||||
|
||||
@@ -26,18 +26,19 @@
|
||||
:style="`width: 100%; height: 100%`"
|
||||
>
|
||||
<CompassHUD
|
||||
v-if="showCompassHUD"
|
||||
v-if="hasCameraFieldOfView"
|
||||
:sun-heading="sunHeading"
|
||||
:camera-angle-of-view="cameraAngleOfView"
|
||||
:camera-pan="cameraPan"
|
||||
/>
|
||||
<CompassRose
|
||||
v-if="showCompassRose"
|
||||
v-if="hasCameraFieldOfView"
|
||||
:camera-angle-of-view="cameraAngleOfView"
|
||||
:camera-pan="cameraPan"
|
||||
:compass-rose-sizing-classes="compassRoseSizingClasses"
|
||||
:heading="heading"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
:sun-heading="sunHeading"
|
||||
:transformations="transformations"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -46,12 +47,18 @@
|
||||
import CompassHUD from './CompassHUD.vue';
|
||||
import CompassRose from './CompassRose.vue';
|
||||
|
||||
const CAMERA_ANGLE_OF_VIEW = 70;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CompassHUD,
|
||||
CompassRose
|
||||
},
|
||||
props: {
|
||||
compassRoseSizingClasses: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
image: {
|
||||
type: Object,
|
||||
required: true
|
||||
@@ -62,19 +69,13 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showCompassHUD() {
|
||||
return this.hasCameraPan && this.cameraAngleOfView > 0;
|
||||
},
|
||||
showCompassRose() {
|
||||
return (this.hasCameraPan || this.hasHeading) && this.cameraAngleOfView > 0;
|
||||
hasCameraFieldOfView() {
|
||||
return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
|
||||
},
|
||||
// horizontal rotation from north in degrees
|
||||
heading() {
|
||||
return this.image.heading;
|
||||
},
|
||||
hasHeading() {
|
||||
return this.heading !== undefined;
|
||||
},
|
||||
// horizontal rotation from north in degrees
|
||||
sunHeading() {
|
||||
return this.image.sunOrientation;
|
||||
@@ -83,14 +84,8 @@ export default {
|
||||
cameraPan() {
|
||||
return this.image.cameraPan;
|
||||
},
|
||||
hasCameraPan() {
|
||||
return this.cameraPan !== undefined;
|
||||
},
|
||||
cameraAngleOfView() {
|
||||
return this.transformations?.cameraAngleOfView;
|
||||
},
|
||||
transformations() {
|
||||
return this.image.transformations;
|
||||
return CAMERA_ANGLE_OF_VIEW;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
class="c-cr__edge"
|
||||
width="100"
|
||||
height="100"
|
||||
fill="url(#gradient_edge)"
|
||||
fill="url(#paint0_radial)"
|
||||
/>
|
||||
<rect
|
||||
v-if="hasSunHeading"
|
||||
class="c-cr__sun"
|
||||
width="100"
|
||||
height="100"
|
||||
fill="url(#gradient_sun)"
|
||||
fill="url(#paint1_radial)"
|
||||
:style="sunHeadingStyle"
|
||||
/>
|
||||
|
||||
@@ -107,26 +107,9 @@
|
||||
height="100"
|
||||
/>
|
||||
</mask>
|
||||
|
||||
<!-- Equipment (spacecraft) body holder. Transforms relative to the camera position. -->
|
||||
<g
|
||||
v-if="hasHeading"
|
||||
class="cr-vrover"
|
||||
:style="camAngleAndPositionStyle"
|
||||
>
|
||||
<!-- Equipment body. Rotates relative to the camera gimbal value for cams that gimbal. -->
|
||||
<path
|
||||
class="cr-vrover__body"
|
||||
:style="camGimbalAngleStyle"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5 0C2.23858 0 0 2.23858 0 5V95C0 97.7614 2.23858 100 5 100H95C97.7614 100 100 97.7614 100 95V5C100 2.23858 97.7614 0 95 0H5ZM85 59L50 24L15 59H33V75H67.0455V59H85Z"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<g
|
||||
class="c-cr__cam-fov"
|
||||
:style="cameraHeadingStyle"
|
||||
:style="cameraPanStyle"
|
||||
>
|
||||
<g mask="url(#mask2)">
|
||||
<rect
|
||||
@@ -145,13 +128,19 @@
|
||||
:style="cameraFOVStyleLeftHalf"
|
||||
/>
|
||||
</g>
|
||||
<polygon
|
||||
class="c-cr__cam"
|
||||
points="0,0 100,0 70,40 70,100 30,100 30,40"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Spacecraft body -->
|
||||
<path
|
||||
v-if="hasHeading"
|
||||
class="c-cr__spacecraft-body"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M37 49C35.3431 49 34 50.3431 34 52V82C34 83.6569 35.3431 85 37 85H63C64.6569 85 66 83.6569 66 82V52C66 50.3431 64.6569 49 63 49H37ZM50 52L58 60H55V67H45V60H42L50 52Z"
|
||||
:style="headingStyle"
|
||||
/>
|
||||
|
||||
<!-- NSEW and ticks -->
|
||||
<g
|
||||
class="c-cr__nsew"
|
||||
@@ -204,7 +193,7 @@
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="gradient_edge"
|
||||
id="paint0_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
@@ -212,7 +201,7 @@
|
||||
gradientTransform="translate(50 50) rotate(90) scale(50)"
|
||||
>
|
||||
<stop
|
||||
offset="0.6"
|
||||
offset="0.751387"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
<stop
|
||||
@@ -221,7 +210,7 @@
|
||||
/>
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="gradient_sun"
|
||||
id="paint1_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
@@ -229,17 +218,12 @@
|
||||
gradientTransform="translate(50 -7) rotate(-90) scale(18.5)"
|
||||
>
|
||||
<stop
|
||||
offset="0.7"
|
||||
offset="0.716377"
|
||||
stop-color="#FFCC00"
|
||||
/>
|
||||
<stop
|
||||
offset="0.7"
|
||||
stop-color="#FFCC00"
|
||||
stop-opacity="0.6"
|
||||
/>
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#FF6600"
|
||||
stop-color="#FF9900"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</radialGradient>
|
||||
@@ -254,6 +238,10 @@ import { throttle } from 'lodash';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
compassRoseSizingClasses: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
heading: {
|
||||
type: Number,
|
||||
required: true,
|
||||
@@ -265,13 +253,16 @@ export default {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraPan: {
|
||||
cameraAngleOfView: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
transformations: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
sizedImageDimensions: {
|
||||
type: Object,
|
||||
@@ -284,38 +275,11 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
cameraHeading() {
|
||||
return this.cameraPan ?? this.heading;
|
||||
},
|
||||
cameraAngleOfView() {
|
||||
const cameraAngleOfView = this.transformations?.cameraAngleOfView;
|
||||
|
||||
if (!cameraAngleOfView) {
|
||||
console.warn('No Camera Angle of View provided');
|
||||
}
|
||||
|
||||
return cameraAngleOfView;
|
||||
},
|
||||
camAngleAndPositionStyle() {
|
||||
const translateX = this.transformations?.translateX;
|
||||
const translateY = this.transformations?.translateY;
|
||||
const rotation = this.transformations?.rotation;
|
||||
const scale = this.transformations?.scale;
|
||||
|
||||
return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` };
|
||||
},
|
||||
camGimbalAngleStyle() {
|
||||
const rotation = rotate(this.north, this.heading);
|
||||
|
||||
return {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
};
|
||||
},
|
||||
compassRoseStyle() {
|
||||
return { transform: `rotate(${ this.north }deg)` };
|
||||
},
|
||||
north() {
|
||||
return this.lockCompass ? rotate(-this.cameraHeading) : 0;
|
||||
return this.lockCompass ? rotate(-this.cameraPan) : 0;
|
||||
},
|
||||
cardinalTextRotateN() {
|
||||
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
|
||||
@@ -333,7 +297,6 @@ export default {
|
||||
return this.heading !== undefined;
|
||||
},
|
||||
headingStyle() {
|
||||
/* Replaced with computed camGimbalStyle, but left here just in case. */
|
||||
const rotation = rotate(this.north, this.heading);
|
||||
|
||||
return {
|
||||
@@ -350,8 +313,8 @@ export default {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
};
|
||||
},
|
||||
cameraHeadingStyle() {
|
||||
const rotation = rotate(this.north, this.cameraHeading);
|
||||
cameraPanStyle() {
|
||||
const rotation = rotate(this.north, this.cameraPan);
|
||||
|
||||
return {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
@@ -370,24 +333,6 @@ export default {
|
||||
return {
|
||||
transform: `rotate(${ -this.cameraAngleOfView / 2 }deg)`
|
||||
};
|
||||
},
|
||||
compassRoseSizingClasses() {
|
||||
let compassRoseSizingClasses = '';
|
||||
if (this.sizedImageWidth < 300) {
|
||||
compassRoseSizingClasses = '--rose-small --rose-min';
|
||||
} else if (this.sizedImageWidth < 500) {
|
||||
compassRoseSizingClasses = '--rose-small';
|
||||
} else if (this.sizedImageWidth > 1000) {
|
||||
compassRoseSizingClasses = '--rose-max';
|
||||
}
|
||||
|
||||
return compassRoseSizingClasses;
|
||||
},
|
||||
sizedImageWidth() {
|
||||
return this.sizedImageDimensions.width;
|
||||
},
|
||||
sizedImageHeight() {
|
||||
return this.sizedImageDimensions.height;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/***************************** THEME/UI CONSTANTS AND MIXINS */
|
||||
$interfaceKeyColor: #fff;
|
||||
$interfaceKeyColor: #00B9C5;
|
||||
$elemBg: rgba(black, 0.7);
|
||||
|
||||
@mixin sun($position: 'circle closest-side') {
|
||||
@@ -100,19 +100,13 @@ $elemBg: rgba(black, 0.7);
|
||||
}
|
||||
|
||||
&__edge {
|
||||
opacity: 0.2;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
&__sun {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&__cam {
|
||||
fill: $interfaceKeyColor;
|
||||
transform-origin: center;
|
||||
transform: scale(0.15);
|
||||
}
|
||||
|
||||
&__cam-fov-l,
|
||||
&__cam-fov-r {
|
||||
// Cam FOV indication
|
||||
@@ -121,6 +115,7 @@ $elemBg: rgba(black, 0.7);
|
||||
}
|
||||
|
||||
&__nsew-text,
|
||||
&__spacecraft-body,
|
||||
&__ticks-major,
|
||||
&__ticks-minor {
|
||||
fill: $color;
|
||||
@@ -171,15 +166,3 @@ $elemBg: rgba(black, 0.7);
|
||||
padding-top: $s;
|
||||
}
|
||||
}
|
||||
|
||||
/************************** ROVER */
|
||||
.cr-vrover {
|
||||
$scale: 0.4;
|
||||
transform-origin: center;
|
||||
|
||||
&__body {
|
||||
fill: $interfaceKeyColor;
|
||||
opacity: 0.3;
|
||||
transform-origin: center 7% !important; // Places rotation center at mast position
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<img
|
||||
ref="img"
|
||||
class="c-thumb__image"
|
||||
:src="`${image.thumbnailUrl || image.url}`"
|
||||
:src="image.url"
|
||||
fetchpriority="low"
|
||||
@load="imageLoadCompleted"
|
||||
>
|
||||
|
||||
@@ -186,17 +186,17 @@ export default {
|
||||
item.remove();
|
||||
});
|
||||
let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
|
||||
imagery.forEach(imageElm => {
|
||||
imagery.forEach(item => {
|
||||
if (clearAllImagery) {
|
||||
imageElm.remove();
|
||||
item.remove();
|
||||
} else {
|
||||
const id = imageElm.getAttributeNS(null, 'id');
|
||||
const id = item.getAttributeNS(null, 'id');
|
||||
if (id) {
|
||||
const timestamp = id.replace(ID_PREFIX, '');
|
||||
if (!this.isImageryInBounds({
|
||||
time: timestamp
|
||||
})) {
|
||||
imageElm.remove();
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,25 +343,25 @@ export default {
|
||||
imageElement.style.display = 'block';
|
||||
}
|
||||
},
|
||||
updateExistingImageWrapper(existingImageWrapper, image, showImagePlaceholders) {
|
||||
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
|
||||
//Update the x co-ordinates of the image wrapper and the url of image
|
||||
//this is to avoid tearing down all elements completely and re-drawing them
|
||||
this.setNSAttributesForElement(existingImageWrapper, {
|
||||
'data-show-image-placeholders': showImagePlaceholders
|
||||
});
|
||||
existingImageWrapper.style.left = `${this.xScale(image.time)}px`;
|
||||
existingImageWrapper.style.left = `${this.xScale(item.time)}px`;
|
||||
|
||||
let imageElement = existingImageWrapper.querySelector('img');
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
src: image.thumbnailUrl || image.url
|
||||
src: item.url
|
||||
});
|
||||
this.setImageDisplay(imageElement, showImagePlaceholders);
|
||||
},
|
||||
createImageWrapper(index, image, showImagePlaceholders) {
|
||||
const id = `${ID_PREFIX}${image.time}`;
|
||||
createImageWrapper(index, item, showImagePlaceholders) {
|
||||
const id = `${ID_PREFIX}${item.time}`;
|
||||
let imageWrapper = document.createElement('div');
|
||||
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
|
||||
imageWrapper.style.left = `${this.xScale(image.time)}px`;
|
||||
imageWrapper.style.left = `${this.xScale(item.time)}px`;
|
||||
this.setNSAttributesForElement(imageWrapper, {
|
||||
id,
|
||||
'data-show-image-placeholders': showImagePlaceholders
|
||||
@@ -383,7 +383,7 @@ export default {
|
||||
//create image element
|
||||
let imageElement = document.createElement('img');
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
src: image.thumbnailUrl || image.url
|
||||
src: item.url
|
||||
});
|
||||
imageElement.style.width = `${IMAGE_SIZE}px`;
|
||||
imageElement.style.height = `${IMAGE_SIZE}px`;
|
||||
@@ -392,7 +392,7 @@ export default {
|
||||
//handle mousedown event to show the image in a large view
|
||||
imageWrapper.addEventListener('mousedown', (e) => {
|
||||
if (e.button === 0) {
|
||||
this.expand(image.time);
|
||||
this.expand(item.time);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
></div>
|
||||
<Compass
|
||||
v-if="shouldDisplayCompass"
|
||||
:compass-rose-sizing-classes="compassRoseSizingClasses"
|
||||
:image="focusedImage"
|
||||
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
@@ -171,7 +172,7 @@
|
||||
>
|
||||
<ImageThumbnail
|
||||
v-for="(image, index) in imageHistory"
|
||||
:key="`${image.thumbnailUrl || image.url}${image.time}`"
|
||||
:key="image.url + image.time"
|
||||
:image="image"
|
||||
:active="focusedImageIndex === index"
|
||||
:selected="focusedImageIndex === index && isPaused"
|
||||
@@ -225,9 +226,6 @@ const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
|
||||
|
||||
const IMAGE_CONTAINER_BORDER_WIDTH = 1;
|
||||
|
||||
const DEFAULT_IMAGE_PAN_ALT_TEXT = "Alt drag to pan";
|
||||
const LINUX_IMAGE_PAN_ALT_TEXT = `Ctrl+${DEFAULT_IMAGE_PAN_ALT_TEXT}`;
|
||||
|
||||
export default {
|
||||
name: 'ImageryView',
|
||||
components: {
|
||||
@@ -300,6 +298,18 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
compassRoseSizingClasses() {
|
||||
let compassRoseSizingClasses = '';
|
||||
if (this.sizedImageWidth < 300) {
|
||||
compassRoseSizingClasses = '--rose-small --rose-min';
|
||||
} else if (this.sizedImageWidth < 500) {
|
||||
compassRoseSizingClasses = '--rose-small';
|
||||
} else if (this.sizedImageWidth > 1000) {
|
||||
compassRoseSizingClasses = '--rose-max';
|
||||
}
|
||||
|
||||
return compassRoseSizingClasses;
|
||||
},
|
||||
displayThumbnails() {
|
||||
return (
|
||||
this.forceShowThumbnails
|
||||
@@ -311,8 +321,8 @@ export default {
|
||||
},
|
||||
focusImageStyles() {
|
||||
return {
|
||||
filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
|
||||
backgroundImage:
|
||||
'filter': `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
|
||||
'background-image':
|
||||
`${this.imageUrl ? (
|
||||
`url(${this.imageUrl}),
|
||||
repeating-linear-gradient(
|
||||
@@ -323,10 +333,10 @@ export default {
|
||||
rgba(125,125,125,.2) 8px
|
||||
)`
|
||||
) : ''}`,
|
||||
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
|
||||
transition: `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
|
||||
width: `${this.sizedImageWidth}px`,
|
||||
height: `${this.sizedImageHeight}px`
|
||||
'transform': `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
|
||||
'transition': `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
|
||||
'width': `${this.sizedImageWidth}px`,
|
||||
'height': `${this.sizedImageHeight}px`
|
||||
};
|
||||
},
|
||||
time() {
|
||||
@@ -337,12 +347,11 @@ export default {
|
||||
},
|
||||
imageWrapperStyle() {
|
||||
return {
|
||||
cursorZoomIn: this.cursorStates.showCursorZoomIn,
|
||||
cursorZoomOut: this.cursorStates.showCursorZoomOut,
|
||||
pannable: this.cursorStates.isPannable,
|
||||
paused: this.isPaused && !this.isFixed,
|
||||
unsynced: this.isPaused && !this.isFixed,
|
||||
stale: false
|
||||
'cursor-zoom-in': this.cursorStates.showCursorZoomIn,
|
||||
'cursor-zoom-out': this.cursorStates.showCursorZoomOut,
|
||||
'pannable': this.cursorStates.isPannable,
|
||||
'paused unnsynced': this.isPaused && !this.isFixed,
|
||||
'stale': false
|
||||
};
|
||||
},
|
||||
isImageNew() {
|
||||
@@ -423,6 +432,7 @@ export default {
|
||||
shouldDisplayCompass() {
|
||||
const imageHeightAndWidth = this.sizedImageHeight !== 0
|
||||
&& this.sizedImageWidth !== 0;
|
||||
|
||||
const display = this.focusedImage !== undefined
|
||||
&& this.focusedImageNaturalAspectRatio !== undefined
|
||||
&& this.imageContainerWidth !== undefined
|
||||
@@ -430,9 +440,8 @@ export default {
|
||||
&& imageHeightAndWidth
|
||||
&& this.zoomFactor === 1
|
||||
&& this.imagePanned !== true;
|
||||
const hasCameraConfigurations = this.focusedImage?.transformations !== undefined;
|
||||
|
||||
return display && hasCameraConfigurations;
|
||||
return display;
|
||||
},
|
||||
isSpacecraftPositionFresh() {
|
||||
let isFresh = undefined;
|
||||
@@ -519,10 +528,10 @@ export default {
|
||||
const navigator = window.navigator.userAgent;
|
||||
|
||||
if (regexLinux.test(navigator)) {
|
||||
return LINUX_IMAGE_PAN_ALT_TEXT;
|
||||
return 'Ctrl+Alt drag to pan';
|
||||
}
|
||||
|
||||
return DEFAULT_IMAGE_PAN_ALT_TEXT;
|
||||
return 'Alt drag to pan';
|
||||
},
|
||||
viewableArea() {
|
||||
if (this.zoomFactor === 1) {
|
||||
@@ -617,7 +626,6 @@ export default {
|
||||
this.spacecraftOrientationKeys = ['heading'];
|
||||
this.cameraKeys = ['cameraPan', 'cameraTilt'];
|
||||
this.sunKeys = ['sunOrientation'];
|
||||
this.transformationsKeys = ['transformations'];
|
||||
|
||||
// related telemetry
|
||||
await this.initializeRelatedTelemetry();
|
||||
@@ -683,9 +691,9 @@ export default {
|
||||
},
|
||||
getVisibleLayerStyles(layer) {
|
||||
return {
|
||||
backgroundImage: `url(${layer.source})`,
|
||||
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
|
||||
transition: `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`
|
||||
'background-image': `url(${layer.source})`,
|
||||
'transform': `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
|
||||
'transition': `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`
|
||||
};
|
||||
},
|
||||
setTimeContext() {
|
||||
@@ -713,20 +721,14 @@ export default {
|
||||
&& visibleActions.find(action => action.key === 'large.view');
|
||||
|
||||
if (viewLargeAction && viewLargeAction.appliesTo(this.objectPath, this.currentView)) {
|
||||
viewLargeAction.invoke(this.objectPath, this.currentView);
|
||||
viewLargeAction.onItemClicked();
|
||||
}
|
||||
},
|
||||
async initializeRelatedTelemetry() {
|
||||
this.relatedTelemetry = new RelatedTelemetry(
|
||||
this.openmct,
|
||||
this.domainObject,
|
||||
[
|
||||
...this.spacecraftPositionKeys,
|
||||
...this.spacecraftOrientationKeys,
|
||||
...this.cameraKeys,
|
||||
...this.sunKeys,
|
||||
...this.transformationsKeys
|
||||
]
|
||||
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys]
|
||||
);
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
@@ -763,30 +765,30 @@ export default {
|
||||
return mostRecent[valueKey];
|
||||
},
|
||||
loadVisibleLayers() {
|
||||
const layersMetadata = this.imageMetadataValue.layers;
|
||||
if (!layersMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.layers = layersMetadata;
|
||||
if (this.domainObject.configuration) {
|
||||
const persistedLayers = this.domainObject.configuration.layers;
|
||||
layersMetadata.forEach((layer) => {
|
||||
const persistedLayer = persistedLayers.find(object => object.name === layer.name);
|
||||
if (persistedLayer) {
|
||||
layer.visible = persistedLayer.visible === true;
|
||||
}
|
||||
});
|
||||
this.visibleLayers = this.layers.filter(layer => layer.visible);
|
||||
} else {
|
||||
this.visibleLayers = [];
|
||||
this.layers.forEach((layer) => {
|
||||
layer.visible = false;
|
||||
});
|
||||
const metaDataValues = this.metadata.valuesForHints(['image'])[0];
|
||||
this.imageFormat = this.openmct.telemetry.getValueFormatter(metaDataValues);
|
||||
let layersMetadata = metaDataValues.layers;
|
||||
if (layersMetadata) {
|
||||
this.layers = layersMetadata;
|
||||
if (this.domainObject.configuration) {
|
||||
let persistedLayers = this.domainObject.configuration.layers;
|
||||
layersMetadata.forEach((layer) => {
|
||||
const persistedLayer = persistedLayers.find(object => object.name === layer.name);
|
||||
if (persistedLayer) {
|
||||
layer.visible = persistedLayer.visible === true;
|
||||
}
|
||||
});
|
||||
this.visibleLayers = this.layers.filter(layer => layer.visible);
|
||||
} else {
|
||||
this.visibleLayers = [];
|
||||
this.layers.forEach((layer) => {
|
||||
layer.visible = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -835,15 +837,6 @@ export default {
|
||||
this.$set(this.focusedImageRelatedTelemetry, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// set configuration for compass
|
||||
this.transformationsKeys.forEach(key => {
|
||||
const transformations = this.relatedTelemetry[key];
|
||||
|
||||
if (transformations !== undefined) {
|
||||
this.$set(this.imageHistory[this.focusedImageIndex], key, transformations);
|
||||
}
|
||||
});
|
||||
},
|
||||
trackLatestRelatedTelemetry() {
|
||||
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&.unsynced{
|
||||
&.unnsynced{
|
||||
@include sUnsynced();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const IMAGE_HINT_KEY = 'image';
|
||||
const IMAGE_THUMBNAIL_HINT_KEY = 'thumbnail';
|
||||
const IMAGE_DOWNLOAD_NAME_HINT_KEY = 'imageDownloadName';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
@@ -35,20 +32,13 @@ export default {
|
||||
this.setDataTimeContext();
|
||||
this.openmct.objectViews.on('clearData', this.dataCleared);
|
||||
|
||||
// Get metadata and formatters
|
||||
// set
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
|
||||
this.imageMetadataValue = { ...this.metadata.valuesForHints([IMAGE_HINT_KEY])[0] };
|
||||
this.imageFormatter = this.getFormatter(this.imageMetadataValue.key);
|
||||
|
||||
this.imageThumbnailMetadataValue = { ...this.metadata.valuesForHints([IMAGE_THUMBNAIL_HINT_KEY])[0] };
|
||||
this.imageThumbnailFormatter = this.imageThumbnailMetadataValue.key
|
||||
? this.getFormatter(this.imageThumbnailMetadataValue.key)
|
||||
: null;
|
||||
|
||||
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageDownloadNameMetadataValue = { ...this.metadata.valuesForHints([IMAGE_DOWNLOAD_NAME_HINT_KEY])[0]};
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
@@ -115,19 +105,12 @@ export default {
|
||||
|
||||
return this.imageFormatter.format(datum);
|
||||
},
|
||||
formatImageThumbnailUrl(datum) {
|
||||
if (!datum || !this.imageThumbnailFormatter) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.imageThumbnailFormatter.format(datum);
|
||||
},
|
||||
formatTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateTimeStr = this.timeFormatter.format(datum);
|
||||
let dateTimeStr = this.timeFormatter.format(datum);
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr.replace("T", " ");
|
||||
@@ -135,7 +118,7 @@ export default {
|
||||
getImageDownloadName(datum) {
|
||||
let imageDownloadName = '';
|
||||
if (datum) {
|
||||
const key = this.imageDownloadNameMetadataValue.key;
|
||||
const key = this.imageDownloadNameHints.key;
|
||||
imageDownloadName = datum[key];
|
||||
}
|
||||
|
||||
@@ -167,7 +150,6 @@ export default {
|
||||
normalizeDatum(datum) {
|
||||
const formattedTime = this.formatTime(datum);
|
||||
const url = this.formatImageUrl(datum);
|
||||
const thumbnailUrl = this.formatImageThumbnailUrl(datum);
|
||||
const time = this.parseTime(formattedTime);
|
||||
const imageDownloadName = this.getImageDownloadName(datum);
|
||||
|
||||
@@ -175,14 +157,13 @@ export default {
|
||||
...datum,
|
||||
formattedTime,
|
||||
url,
|
||||
thumbnailUrl,
|
||||
time,
|
||||
imageDownloadName
|
||||
};
|
||||
},
|
||||
getFormatter(key) {
|
||||
const metadataValue = this.metadata.value(key) || { format: key };
|
||||
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
let metadataValue = this.metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
}
|
||||
|
||||
@@ -35,10 +35,6 @@ const MAIN_IMAGE_CLASS = '.js-imageryView-image';
|
||||
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
|
||||
const REFRESH_CSS_MS = 500;
|
||||
|
||||
function formatThumbnail(url) {
|
||||
return url.replace('logo-openmct.svg', 'logo-nasa.svg');
|
||||
}
|
||||
|
||||
function getImageInfo(doc) {
|
||||
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
||||
let timestamp = imageElement.dataset.openmctImageTimestamp;
|
||||
@@ -128,16 +124,6 @@ describe("The Imagery View Layouts", () => {
|
||||
},
|
||||
"source": "url"
|
||||
},
|
||||
{
|
||||
"name": "Image Thumbnail",
|
||||
"key": "thumbnail-url",
|
||||
"format": "thumbnail",
|
||||
"hints": {
|
||||
"thumbnail": 1,
|
||||
"priority": 3
|
||||
},
|
||||
"source": "url"
|
||||
},
|
||||
{
|
||||
"name": "Name",
|
||||
"key": "name",
|
||||
@@ -214,11 +200,6 @@ describe("The Imagery View Layouts", () => {
|
||||
|
||||
originalRouterPath = openmct.router.path;
|
||||
|
||||
openmct.telemetry.addFormat({
|
||||
key: 'thumbnail',
|
||||
format: formatThumbnail
|
||||
});
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
@@ -403,32 +384,15 @@ describe("The Imagery View Layouts", () => {
|
||||
//Looks like we need Vue.nextTick here so that computed properties settle down
|
||||
await Vue.nextTick();
|
||||
const layerEls = parent.querySelectorAll('.js-layer-image');
|
||||
console.log(layerEls);
|
||||
expect(layerEls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should use the image thumbnailUrl for thumbnails", async () => {
|
||||
await Vue.nextTick();
|
||||
const fullSizeImageUrl = imageTelemetry[5].url;
|
||||
const thumbnailUrl = formatThumbnail(imageTelemetry[5].url);
|
||||
|
||||
// Ensure thumbnails are shown w/ thumbnail Urls
|
||||
const thumbnails = parent.querySelectorAll(`img[src='${thumbnailUrl}']`);
|
||||
expect(thumbnails.length).toBeGreaterThan(0);
|
||||
|
||||
// Click a thumbnail
|
||||
parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click();
|
||||
await Vue.nextTick();
|
||||
|
||||
// Ensure full size image is shown w/ full size url
|
||||
const fullSizeImages = parent.querySelectorAll(`img[src='${fullSizeImageUrl}']`);
|
||||
expect(fullSizeImages.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should show the clicked thumbnail as the main image", async () => {
|
||||
//Looks like we need Vue.nextTick here so that computed properties settle down
|
||||
await Vue.nextTick();
|
||||
const thumbnailUrl = formatThumbnail(imageTelemetry[5].url);
|
||||
parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click();
|
||||
const target = imageTelemetry[5].url;
|
||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||
await Vue.nextTick();
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
@@ -453,7 +417,7 @@ describe("The Imagery View Layouts", () => {
|
||||
|
||||
it("should show that an image is not new", async () => {
|
||||
await Vue.nextTick();
|
||||
const target = formatThumbnail(imageTelemetry[4].url);
|
||||
const target = imageTelemetry[4].url;
|
||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||
|
||||
await Vue.nextTick();
|
||||
|
||||
@@ -30,7 +30,6 @@ export default class LinkAction {
|
||||
this.priority = 7;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.transaction = null;
|
||||
}
|
||||
|
||||
appliesTo(objectPath) {
|
||||
@@ -49,9 +48,7 @@ export default class LinkAction {
|
||||
}
|
||||
|
||||
onSave(changes) {
|
||||
this.startTransaction();
|
||||
|
||||
const inNavigationPath = this.inNavigationPath();
|
||||
let inNavigationPath = this.inNavigationPath();
|
||||
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
}
|
||||
@@ -60,8 +57,6 @@ export default class LinkAction {
|
||||
const parent = parentDomainObjectpath[0];
|
||||
|
||||
this.linkInNewParent(this.object, parent);
|
||||
|
||||
return this.saveTransaction();
|
||||
}
|
||||
|
||||
linkInNewParent(child, newParent) {
|
||||
@@ -133,19 +128,4 @@ export default class LinkAction {
|
||||
return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object);
|
||||
};
|
||||
}
|
||||
startTransaction() {
|
||||
if (!this.openmct.objects.isTransactionActive()) {
|
||||
this.transaction = this.openmct.objects.startTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
async saveTransaction() {
|
||||
if (!this.transaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.transaction.commit();
|
||||
this.openmct.objects.endTransaction();
|
||||
this.transaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ export default class MoveAction {
|
||||
this.priority = 7;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.transaction = null;
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
@@ -61,8 +60,6 @@ export default class MoveAction {
|
||||
}
|
||||
|
||||
async onSave(changes) {
|
||||
this.startTransaction();
|
||||
|
||||
let inNavigationPath = this.inNavigationPath(this.object);
|
||||
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
@@ -102,8 +99,6 @@ export default class MoveAction {
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveTransaction();
|
||||
|
||||
this.navigateTo(newObjectPath);
|
||||
}
|
||||
|
||||
@@ -194,20 +189,4 @@ export default class MoveAction {
|
||||
&& childType.definition.creatable
|
||||
&& Array.isArray(parent.composition);
|
||||
}
|
||||
|
||||
startTransaction() {
|
||||
if (!this.openmct.objects.isTransactionActive()) {
|
||||
this.transaction = this.openmct.objects.startTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
async saveTransaction() {
|
||||
if (!this.transaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.transaction.commit();
|
||||
this.openmct.objects.endTransaction();
|
||||
this.transaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,13 @@ import Notebook from './components/Notebook.vue';
|
||||
import Agent from '@/utils/agent/Agent';
|
||||
|
||||
export default class NotebookViewProvider {
|
||||
constructor(openmct, name, key, type, cssClass, snapshotContainer, entryUrlWhitelist) {
|
||||
constructor(openmct, name, key, type, cssClass, snapshotContainer) {
|
||||
this.openmct = openmct;
|
||||
this.key = key;
|
||||
this.name = `${name} View`;
|
||||
this.type = type;
|
||||
this.cssClass = cssClass;
|
||||
this.snapshotContainer = snapshotContainer;
|
||||
this.entryUrlWhitelist = entryUrlWhitelist;
|
||||
}
|
||||
|
||||
canView(domainObject) {
|
||||
@@ -44,7 +43,6 @@ export default class NotebookViewProvider {
|
||||
let openmct = this.openmct;
|
||||
let snapshotContainer = this.snapshotContainer;
|
||||
let agent = new Agent(window);
|
||||
let entryUrlWhitelist = this.entryUrlWhitelist;
|
||||
|
||||
return {
|
||||
show(container) {
|
||||
@@ -56,8 +54,7 @@ export default class NotebookViewProvider {
|
||||
provide: {
|
||||
openmct,
|
||||
snapshotContainer,
|
||||
agent,
|
||||
entryUrlWhitelist
|
||||
agent
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -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"
|
||||
@@ -162,12 +156,10 @@
|
||||
:selected-section="selectedSection"
|
||||
:read-only="false"
|
||||
:is-locked="selectedPage.isLocked"
|
||||
:selected-entry-id="selectedEntryId"
|
||||
@cancelEdit="cancelTransaction"
|
||||
@editingEntry="startTransaction"
|
||||
@deleteEntry="deleteEntry"
|
||||
@updateEntry="updateEntry"
|
||||
@entry-selection="entrySelection(entry)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -191,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';
|
||||
@@ -209,8 +200,7 @@ export default {
|
||||
NotebookEntry,
|
||||
Search,
|
||||
SearchResults,
|
||||
Sidebar,
|
||||
ProgressBar
|
||||
Sidebar
|
||||
},
|
||||
inject: ['agent', 'openmct', 'snapshotContainer'],
|
||||
props: {
|
||||
@@ -235,10 +225,7 @@ export default {
|
||||
showNav: false,
|
||||
sidebarCoversEntries: false,
|
||||
filteredAndSortedEntries: [],
|
||||
notebookAnnotations: {},
|
||||
selectedEntryId: '',
|
||||
activeTransaction: false,
|
||||
savingTransaction: false
|
||||
notebookAnnotations: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -283,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);
|
||||
|
||||
@@ -324,9 +297,6 @@ export default {
|
||||
this.formatSidebar();
|
||||
this.setSectionAndPageFromUrl();
|
||||
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.transaction = null;
|
||||
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.filterAndSortEntries();
|
||||
@@ -350,7 +320,6 @@ export default {
|
||||
|
||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
},
|
||||
updated: function () {
|
||||
this.$nextTick(() => {
|
||||
@@ -380,20 +349,15 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
updateSelection(selection) {
|
||||
if (selection?.[0]?.[1]?.context?.targetDetails?.entryId === undefined) {
|
||||
this.selectedEntryId = '';
|
||||
}
|
||||
},
|
||||
async loadAnnotations() {
|
||||
if (!this.openmct.annotation.getAvailableTags().length) {
|
||||
// don't bother loading annotations if there are no tags
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0;
|
||||
|
||||
const foundAnnotations = await this.openmct.annotation.getAnnotations(this.domainObject.identifier);
|
||||
const query = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
const foundAnnotations = await this.openmct.annotation.getAnnotations(query);
|
||||
foundAnnotations.forEach((foundAnnotation) => {
|
||||
const targetId = Object.keys(foundAnnotation.targets)[0];
|
||||
const entryId = foundAnnotation.targets[targetId].entryId;
|
||||
@@ -785,7 +749,6 @@ export default {
|
||||
return section.id;
|
||||
},
|
||||
async newEntry(embed = null) {
|
||||
this.startTransaction();
|
||||
this.resetSearch();
|
||||
const notebookStorage = this.createNotebookStorageObject();
|
||||
this.updateDefaultNotebook(notebookStorage);
|
||||
@@ -928,37 +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();
|
||||
}
|
||||
},
|
||||
entrySelection(entry) {
|
||||
this.selectedEntryId = entry.id;
|
||||
},
|
||||
endTransaction() {
|
||||
this.openmct.objects.endTransaction();
|
||||
this.transaction = null;
|
||||
this.savingTransaction = false;
|
||||
this.activeTransaction = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,15 +12,14 @@
|
||||
<a
|
||||
class="c-ne__embed__link"
|
||||
:class="embed.cssClass"
|
||||
@click="navigateToItemInTime"
|
||||
@click="changeLocation"
|
||||
>{{ embed.name }}</a>
|
||||
<button
|
||||
class="c-ne__embed__actions c-icon-button icon-3-dots"
|
||||
title="More options"
|
||||
@click.prevent.stop="showMenuItems($event)"
|
||||
></button>
|
||||
<PopupMenu :popup-menu-items="popupMenuItems" />
|
||||
</div>
|
||||
<div class="c-ne__embed__time">
|
||||
<div
|
||||
v-if="embed.snapshot"
|
||||
class="c-ne__embed__time"
|
||||
>
|
||||
{{ createdOn }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,14 +32,17 @@ import PreviewAction from '../../../ui/preview/PreviewAction';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import PainterroInstance from '../utils/painterroInstance';
|
||||
import SnapshotTemplate from './snapshot-template.html';
|
||||
import objectPathToUrl from '@/tools/url';
|
||||
|
||||
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
|
||||
import ImageExporter from '../../../exporters/ImageExporter';
|
||||
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PopupMenu
|
||||
},
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
props: {
|
||||
embed: {
|
||||
@@ -70,7 +72,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menuActions: []
|
||||
popupMenuItems: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -86,89 +88,38 @@ export default {
|
||||
watch: {
|
||||
isLocked(value) {
|
||||
if (value === true) {
|
||||
let index = this.menuActions.findIndex((item) => item.id === 'removeEmbed');
|
||||
let index = this.popupMenuItems.findIndex((item) => item.id === 'removeEmbed');
|
||||
|
||||
this.$delete(this.menuActions, index);
|
||||
this.$delete(this.popupMenuItems, index);
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.objectPath = [];
|
||||
await this.setEmbedObjectPath();
|
||||
this.addMenuActions();
|
||||
mounted() {
|
||||
this.addPopupMenuItems();
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
},
|
||||
methods: {
|
||||
showMenuItems(event) {
|
||||
const x = event.x;
|
||||
const y = event.y;
|
||||
|
||||
const menuOptions = {
|
||||
menuClass: 'c-ne__embed__actions-menu',
|
||||
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
|
||||
addPopupMenuItems() {
|
||||
const removeEmbed = {
|
||||
id: 'removeEmbed',
|
||||
cssClass: 'icon-trash',
|
||||
name: this.removeActionString,
|
||||
callback: this.getRemoveDialog.bind(this)
|
||||
};
|
||||
|
||||
this.openmct.menus.showSuperMenu(x, y, this.menuActions, menuOptions);
|
||||
},
|
||||
addMenuActions() {
|
||||
if (this.embed.snapshot) {
|
||||
const viewSnapshot = {
|
||||
id: 'viewSnapshot',
|
||||
cssClass: 'icon-camera',
|
||||
name: 'View Snapshot',
|
||||
description: 'View the snapshot image taken in the form of a jpeg.',
|
||||
onItemClicked: () => this.openSnapshot()
|
||||
};
|
||||
|
||||
this.menuActions = [viewSnapshot];
|
||||
}
|
||||
|
||||
const navigateToItem = {
|
||||
id: 'navigateToItem',
|
||||
cssClass: this.embed.cssClass,
|
||||
name: 'Navigate to Item',
|
||||
description: 'Navigate to the item with the current time settings.',
|
||||
onItemClicked: () => this.navigateToItem()
|
||||
};
|
||||
|
||||
const navigateToItemInTime = {
|
||||
id: 'navigateToItemInTime',
|
||||
cssClass: this.embed.cssClass,
|
||||
name: 'Navigate to Item in Time',
|
||||
description: 'Navigate to the item in its time frame when captured.',
|
||||
onItemClicked: () => this.navigateToItemInTime()
|
||||
};
|
||||
|
||||
const quickView = {
|
||||
id: 'quickView',
|
||||
const preview = {
|
||||
id: 'preview',
|
||||
cssClass: 'icon-eye-open',
|
||||
name: 'Quick View',
|
||||
description: 'Full screen overlay view of the item.',
|
||||
onItemClicked: () => this.previewEmbed()
|
||||
name: 'Preview',
|
||||
callback: this.previewEmbed.bind(this)
|
||||
};
|
||||
|
||||
this.menuActions = this.menuActions.concat([quickView, navigateToItem, navigateToItemInTime]);
|
||||
this.popupMenuItems = [preview];
|
||||
|
||||
if (!this.isLocked) {
|
||||
const removeEmbed = {
|
||||
id: 'removeEmbed',
|
||||
cssClass: 'icon-trash',
|
||||
name: this.removeActionString,
|
||||
description: 'Permanently delete this embed from this Notebook entry.',
|
||||
onItemClicked: this.getRemoveDialog.bind(this)
|
||||
};
|
||||
|
||||
this.menuActions.push(removeEmbed);
|
||||
this.popupMenuItems.unshift(removeEmbed);
|
||||
}
|
||||
|
||||
},
|
||||
async setEmbedObjectPath() {
|
||||
this.objectPath = await this.openmct.objects.getOriginalPath(this.embed.domainObject.identifier);
|
||||
|
||||
if (this.objectPath.length > 0 && this.objectPath[this.objectPath.length - 1].type === 'root') {
|
||||
this.objectPath.pop();
|
||||
}
|
||||
},
|
||||
annotateSnapshot() {
|
||||
const annotateVue = new Vue({
|
||||
template: '<div id="snap-annotation"></div>'
|
||||
@@ -228,11 +179,7 @@ export default {
|
||||
painterroInstance.show(object.configuration.fullSizeImageURL);
|
||||
});
|
||||
},
|
||||
navigateToItem() {
|
||||
const url = objectPathToUrl(this.openmct, this.objectPath);
|
||||
this.openmct.router.navigate(url);
|
||||
},
|
||||
navigateToItemInTime() {
|
||||
changeLocation() {
|
||||
const hash = this.embed.historicLink;
|
||||
|
||||
const bounds = this.openmct.time.bounds();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
@@ -23,37 +22,23 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-notebook__entry c-ne has-local-controls"
|
||||
class="c-notebook__entry c-ne has-local-controls has-tag-applier"
|
||||
aria-label="Notebook Entry"
|
||||
:class="{ 'locked': isLocked, 'is-selected': isSelectedEntry }"
|
||||
:class="{ 'locked': isLocked }"
|
||||
@dragover="changeCursor"
|
||||
@drop.capture="cancelEditMode"
|
||||
@drop.prevent="dropOnEntry"
|
||||
@click="selectEntry($event, entry)"
|
||||
>
|
||||
<div class="c-ne__time-and-content">
|
||||
<div class="c-ne__time-and-creator-and-delete">
|
||||
<div class="c-ne__time-and-creator">
|
||||
<span class="c-ne__created-date">{{ createdOnDate }}</span>
|
||||
<span class="c-ne__created-time">{{ createdOnTime }}</span>
|
||||
<span
|
||||
v-if="entry.createdBy"
|
||||
class="c-ne__creator"
|
||||
>
|
||||
<span class="icon-person"></span> {{ entry.createdBy }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="c-ne__time-and-creator">
|
||||
<span class="c-ne__created-date">{{ createdOnDate }}</span>
|
||||
<span class="c-ne__created-time">{{ createdOnTime }}</span>
|
||||
|
||||
<span
|
||||
v-if="!readOnly && !isLocked"
|
||||
class="c-ne__local-controls--hidden"
|
||||
v-if="entry.createdBy"
|
||||
class="c-ne__creator"
|
||||
>
|
||||
<button
|
||||
class="c-ne__remove c-icon-button c-icon-button--major icon-trash"
|
||||
title="Delete this entry"
|
||||
tabindex="-1"
|
||||
@click="deleteEntry"
|
||||
>
|
||||
</button>
|
||||
<span class="icon-person"></span> {{ entry.createdBy }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="c-ne__content">
|
||||
@@ -76,14 +61,12 @@
|
||||
class="c-ne__text c-ne__input"
|
||||
aria-label="Notebook Entry Input"
|
||||
tabindex="0"
|
||||
:contenteditable="canEdit"
|
||||
@mouseover="checkEditability($event)"
|
||||
@mouseleave="canEdit = true"
|
||||
contenteditable="true"
|
||||
@focus="editingEntry()"
|
||||
@blur="updateEntryValue($event)"
|
||||
@keydown.enter.exact.prevent
|
||||
@keyup.enter.exact.prevent="forceBlur($event)"
|
||||
v-html="formattedText"
|
||||
v-text="entry.text"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
@@ -94,42 +77,43 @@
|
||||
class="c-ne__text"
|
||||
contenteditable="false"
|
||||
tabindex="0"
|
||||
v-html="formattedText"
|
||||
v-text="entry.text"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<div
|
||||
v-for="(tag, index) in entryTags"
|
||||
:key="index"
|
||||
class="c-tag"
|
||||
:style="{ backgroundColor: tag.backgroundColor, color: tag.foregroundColor }"
|
||||
>
|
||||
{{ tag.label }}
|
||||
</div>
|
||||
</div>
|
||||
<TagEditor
|
||||
:domain-object="domainObject"
|
||||
:annotations="notebookAnnotations"
|
||||
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
||||
:target-specific-details="{entryId: entry.id}"
|
||||
@tags-updated="timestampAndUpdate"
|
||||
/>
|
||||
|
||||
<div
|
||||
:class="{'c-scrollcontainer': enableEmbedsWrapperScroll }"
|
||||
>
|
||||
<div
|
||||
ref="embedsWrapper"
|
||||
class="c-snapshots c-ne__embeds-wrapper"
|
||||
>
|
||||
<NotebookEmbed
|
||||
v-for="embed in entry.embeds"
|
||||
ref="embeds"
|
||||
:key="embed.id"
|
||||
:embed="embed"
|
||||
:is-locked="isLocked"
|
||||
@removeEmbed="removeEmbed"
|
||||
@updateEmbed="updateEmbed"
|
||||
/>
|
||||
</div>
|
||||
<div class="c-snapshots c-ne__embeds">
|
||||
<NotebookEmbed
|
||||
v-for="embed in entry.embeds"
|
||||
:key="embed.id"
|
||||
:embed="embed"
|
||||
:is-locked="isLocked"
|
||||
@removeEmbed="removeEmbed"
|
||||
@updateEmbed="updateEmbed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!readOnly && !isLocked"
|
||||
class="c-ne__local-controls--hidden"
|
||||
>
|
||||
<button
|
||||
class="c-icon-button c-icon-button--major icon-trash"
|
||||
title="Delete this entry"
|
||||
tabindex="-1"
|
||||
@click="deleteEntry"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="readOnly"
|
||||
class="c-ne__section-and-page"
|
||||
@@ -155,28 +139,22 @@
|
||||
|
||||
<script>
|
||||
import NotebookEmbed from './NotebookEmbed.vue';
|
||||
import TagEditor from '../../../ui/components/tags/TagEditor.vue';
|
||||
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
|
||||
import { createNewEmbed } from '../utils/notebook-entries';
|
||||
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
||||
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Moment from 'moment';
|
||||
|
||||
const SANITIZATION_SCHEMA = {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {}
|
||||
};
|
||||
const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
|
||||
const UNKNOWN_USER = 'Unknown';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotebookEmbed,
|
||||
TextHighlight
|
||||
TextHighlight,
|
||||
TagEditor
|
||||
},
|
||||
inject: ['openmct', 'snapshotContainer', 'entryUrlWhitelist'],
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@@ -225,19 +203,8 @@ export default {
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
selectedEntryId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editMode: false,
|
||||
canEdit: true,
|
||||
enableEmbedsWrapperScroll: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
createdOnDate() {
|
||||
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
|
||||
@@ -245,39 +212,6 @@ export default {
|
||||
createdOnTime() {
|
||||
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
||||
},
|
||||
formattedText() {
|
||||
// remove ANY tags
|
||||
let text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA);
|
||||
|
||||
if (this.editMode || !this.urlWhitelist) {
|
||||
return text;
|
||||
}
|
||||
|
||||
text = text.replace(URL_REGEX, (match) => {
|
||||
const url = new URL(match);
|
||||
const domain = url.hostname;
|
||||
let result = match;
|
||||
let isMatch = this.urlWhitelist.find((partialDomain) => {
|
||||
return domain.endsWith(partialDomain);
|
||||
});
|
||||
|
||||
if (isMatch) {
|
||||
result = `<a class="c-hyperlink" target="_blank" href="${match}">${match}</a>`;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return text;
|
||||
},
|
||||
isSelectedEntry() {
|
||||
return this.selectedEntryId === this.entry.id;
|
||||
},
|
||||
entryTags() {
|
||||
const tagsFromAnnotations = this.openmct.annotation.getTagsFromAnnotations(this.notebookAnnotations);
|
||||
|
||||
return tagsFromAnnotations;
|
||||
},
|
||||
entryText() {
|
||||
let text = this.entry.text;
|
||||
|
||||
@@ -298,23 +232,7 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.manageEmbedLayout = _.debounce(this.manageEmbedLayout, 400);
|
||||
|
||||
if (this.$refs.embedsWrapper) {
|
||||
this.embedsWrapperResizeObserver = new ResizeObserver(this.manageEmbedLayout);
|
||||
this.embedsWrapperResizeObserver.observe(this.$refs.embedsWrapper);
|
||||
}
|
||||
|
||||
this.manageEmbedLayout();
|
||||
this.dropOnEntry = this.dropOnEntry.bind(this);
|
||||
if (this.entryUrlWhitelist?.length > 0) {
|
||||
this.urlWhitelist = this.entryUrlWhitelist;
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.embedsWrapperResizeObserver) {
|
||||
this.embedsWrapperResizeObserver.unobserve(this.$refs.embedsWrapper);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async addNewEmbed(objectPath) {
|
||||
@@ -327,8 +245,6 @@ export default {
|
||||
};
|
||||
const newEmbed = await createNewEmbed(snapshotMeta);
|
||||
this.entry.embeds.push(newEmbed);
|
||||
|
||||
this.manageEmbedLayout();
|
||||
},
|
||||
cancelEditMode(event) {
|
||||
const isEditing = this.openmct.editor.isEditing();
|
||||
@@ -346,25 +262,9 @@ export default {
|
||||
event.dataTransfer.effectAllowed = 'none';
|
||||
}
|
||||
},
|
||||
checkEditability($event) {
|
||||
if ($event.target.nodeName === 'A') {
|
||||
this.canEdit = false;
|
||||
}
|
||||
},
|
||||
deleteEntry() {
|
||||
this.$emit('deleteEntry', this.entry.id);
|
||||
},
|
||||
manageEmbedLayout() {
|
||||
if (this.$refs.embeds) {
|
||||
const embedsWrapperLength = this.$refs.embedsWrapper.clientWidth;
|
||||
const embedsTotalWidth = this.$refs.embeds.reduce((total, embed) => {
|
||||
return embed.$el.clientWidth + total;
|
||||
}, 0);
|
||||
|
||||
this.enableEmbedsWrapperScroll = embedsTotalWidth > embedsWrapperLength;
|
||||
}
|
||||
|
||||
},
|
||||
async dropOnEntry($event) {
|
||||
$event.stopImmediatePropagation();
|
||||
|
||||
@@ -422,8 +322,6 @@ export default {
|
||||
this.entry.embeds.splice(embedPosition, 1);
|
||||
|
||||
this.timestampAndUpdate();
|
||||
|
||||
this.manageEmbedLayout();
|
||||
},
|
||||
updateEmbed(newEmbed) {
|
||||
this.entry.embeds.some(e => {
|
||||
@@ -449,11 +347,9 @@ export default {
|
||||
this.$emit('updateEntry', this.entry);
|
||||
},
|
||||
editingEntry() {
|
||||
this.editMode = true;
|
||||
this.$emit('editingEntry');
|
||||
},
|
||||
updateEntryValue($event) {
|
||||
this.editMode = false;
|
||||
const value = $event.target.innerText;
|
||||
if (value !== this.entry.text && value.match(/\S/)) {
|
||||
this.entry.text = value;
|
||||
@@ -461,38 +357,6 @@ export default {
|
||||
} else {
|
||||
this.$emit('cancelEdit');
|
||||
}
|
||||
},
|
||||
selectEntry(event, entry) {
|
||||
const targetDetails = {};
|
||||
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
targetDetails[keyString] = {
|
||||
entryId: entry.id
|
||||
};
|
||||
const targetDomainObjects = {};
|
||||
targetDomainObjects[keyString] = this.domainObject;
|
||||
this.openmct.selection.select(
|
||||
[
|
||||
{
|
||||
element: this.openmct.layout.$refs.browseObject.$el,
|
||||
context: {
|
||||
item: this.domainObject
|
||||
}
|
||||
},
|
||||
{
|
||||
element: event.currentTarget,
|
||||
context: {
|
||||
type: 'notebook-entry-selection',
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations: this.notebookAnnotations,
|
||||
annotationType: this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
onAnnotationChange: this.timestampAndUpdate
|
||||
}
|
||||
}
|
||||
],
|
||||
false);
|
||||
event.stopPropagation();
|
||||
this.$emit('entry-selection', this.entry);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ function installBaseNotebookFunctionality(openmct) {
|
||||
monkeyPatchObjectAPIForNotebooks(openmct);
|
||||
}
|
||||
|
||||
function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {
|
||||
function NotebookPlugin(name = 'Notebook') {
|
||||
return function install(openmct) {
|
||||
if (openmct[NOTEBOOK_INSTALLED_KEY]) {
|
||||
return;
|
||||
@@ -118,8 +118,8 @@ function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {
|
||||
const notebookType = new NotebookType(name, description, icon);
|
||||
openmct.types.addType(NOTEBOOK_TYPE, notebookType);
|
||||
|
||||
const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist);
|
||||
openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);
|
||||
const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer);
|
||||
openmct.objectViews.addProvider(notebookView);
|
||||
|
||||
installBaseNotebookFunctionality(openmct);
|
||||
|
||||
@@ -127,7 +127,7 @@ function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {
|
||||
};
|
||||
}
|
||||
|
||||
function RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist = []) {
|
||||
function RestrictedNotebookPlugin(name = 'Notebook Shift Log') {
|
||||
return function install(openmct) {
|
||||
if (openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) {
|
||||
return;
|
||||
@@ -140,8 +140,8 @@ function RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist
|
||||
const notebookType = new NotebookType(name, description, icon);
|
||||
openmct.types.addType(RESTRICTED_NOTEBOOK_TYPE, notebookType);
|
||||
|
||||
const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist);
|
||||
openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);
|
||||
const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer);
|
||||
openmct.objectViews.addProvider(notebookView);
|
||||
|
||||
installBaseNotebookFunctionality(openmct);
|
||||
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="notifications.length === 0 ? showNotificationsOverlay : notifications.length > 0"
|
||||
v-if="notifications.length > 0"
|
||||
class="c-indicator c-indicator--clickable icon-bell"
|
||||
: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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -88,14 +88,6 @@
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
[class*='__label'] {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
[class*='__poll-table'] {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
[class*='new-question'] {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -131,12 +123,6 @@
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
&__actions {
|
||||
display:flex;
|
||||
flex: auto;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.c-indicator {
|
||||
|
||||
@@ -58,13 +58,6 @@
|
||||
{{ entry.roleCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-status-poll-report__actions">
|
||||
<button
|
||||
class="c-button"
|
||||
title="Clear the previous poll question"
|
||||
@click="clearPollQuestion"
|
||||
>Clear Poll</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -81,41 +74,6 @@
|
||||
@click="updatePollQuestion"
|
||||
>Update</button>
|
||||
</div>
|
||||
<div class="c-table c-spq__poll-table">
|
||||
<table class="c-table__body">
|
||||
<thead class="c-table__header">
|
||||
<tr>
|
||||
<th>
|
||||
Position
|
||||
</th>
|
||||
<th>
|
||||
Status
|
||||
</th>
|
||||
<th>
|
||||
Age
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="statusForRole in statusesForRolesViewModel"
|
||||
:key="statusForRole.key"
|
||||
>
|
||||
<td>
|
||||
{{ statusForRole.role }}
|
||||
</td>
|
||||
<td
|
||||
:style="{ background: statusForRole.status.statusBgColor, color: statusForRole.status.statusFgColor }"
|
||||
>
|
||||
{{ statusForRole.status.label }}
|
||||
</td>
|
||||
<td>
|
||||
{{ statusForRole.age }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -139,11 +97,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
pollQuestionUpdated: '--',
|
||||
pollQuestionTimestamp: undefined,
|
||||
currentPollQuestion: '--',
|
||||
newPollQuestion: undefined,
|
||||
statusCountViewModel: [],
|
||||
statusesForRolesViewModel: []
|
||||
statusCountViewModel: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -179,17 +135,9 @@ export default {
|
||||
this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
|
||||
},
|
||||
setPollQuestion(pollQuestion) {
|
||||
let pollQuestionText = pollQuestion.question;
|
||||
if (!pollQuestionText || pollQuestionText === '') {
|
||||
pollQuestionText = '--';
|
||||
this.indicator.text('No Poll Question');
|
||||
} else {
|
||||
this.indicator.text(pollQuestionText);
|
||||
}
|
||||
|
||||
this.currentPollQuestion = pollQuestionText;
|
||||
this.pollQuestionTimestamp = pollQuestion.timestamp;
|
||||
this.currentPollQuestion = pollQuestion.question;
|
||||
this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();
|
||||
this.indicator.text(pollQuestion.question);
|
||||
},
|
||||
async updatePollQuestion() {
|
||||
const result = await this.openmct.user.status.setPollQuestion(this.newPollQuestion);
|
||||
@@ -201,13 +149,6 @@ export default {
|
||||
|
||||
this.newPollQuestion = undefined;
|
||||
},
|
||||
async clearPollQuestion() {
|
||||
this.currentPollQuestion = undefined;
|
||||
await Promise.all([
|
||||
this.openmct.user.status.resetAllStatuses(),
|
||||
this.openmct.user.status.setPollQuestion()
|
||||
]);
|
||||
},
|
||||
async fetchStatusSummary() {
|
||||
const allStatuses = await this.openmct.user.status.getPossibleStatuses();
|
||||
const statusCountMap = allStatuses.reduce((statusToCountMap, status) => {
|
||||
@@ -217,6 +158,7 @@ export default {
|
||||
}, {});
|
||||
const allStatusRoles = await this.openmct.user.status.getAllStatusRoles();
|
||||
const statusesForRoles = await Promise.all(allStatusRoles.map(role => this.openmct.user.status.getStatusForRole(role)));
|
||||
|
||||
statusesForRoles.forEach((status, i) => {
|
||||
const currentCount = statusCountMap[status.key];
|
||||
statusCountMap[status.key] = currentCount + 1;
|
||||
@@ -228,51 +170,6 @@ export default {
|
||||
roleCount: statusCountMap[status.key]
|
||||
};
|
||||
});
|
||||
const defaultStatuses = await Promise.all(allStatusRoles.map(role => this.openmct.user.status.getDefaultStatusForRole(role)));
|
||||
this.statusesForRolesViewModel = [];
|
||||
statusesForRoles.forEach((status, index) => {
|
||||
const isDefaultStatus = defaultStatuses[index].key === status.key;
|
||||
let statusTimestamp = status.timestamp;
|
||||
if (isDefaultStatus) {
|
||||
// if the default is selected, set timestamp to undefined
|
||||
statusTimestamp = undefined;
|
||||
}
|
||||
|
||||
this.statusesForRolesViewModel.push({
|
||||
status: this.applyStyling(status),
|
||||
role: allStatusRoles[index],
|
||||
age: this.formatStatusAge(statusTimestamp, this.pollQuestionTimestamp)
|
||||
});
|
||||
});
|
||||
},
|
||||
formatStatusAge(statusTimestamp, pollQuestionTimestamp) {
|
||||
if (statusTimestamp === undefined || pollQuestionTimestamp === undefined) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
const statusAgeInMs = statusTimestamp - pollQuestionTimestamp;
|
||||
const absoluteTotalSeconds = Math.floor(Math.abs(statusAgeInMs) / 1000);
|
||||
let hours = Math.floor(absoluteTotalSeconds / 3600);
|
||||
let minutes = Math.floor((absoluteTotalSeconds - (hours * 3600)) / 60);
|
||||
let secondsString = absoluteTotalSeconds - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (statusAgeInMs > 0 || (absoluteTotalSeconds === 0)) {
|
||||
hours = `+ ${hours}`;
|
||||
} else {
|
||||
hours = `- ${hours}`;
|
||||
}
|
||||
|
||||
if (minutes < 10) {
|
||||
minutes = `0${minutes}`;
|
||||
}
|
||||
|
||||
if (secondsString < 10) {
|
||||
secondsString = `0${secondsString}`;
|
||||
}
|
||||
|
||||
const statusAgeString = `${hours}:${minutes}:${secondsString}`;
|
||||
|
||||
return statusAgeString;
|
||||
},
|
||||
applyStyling(status) {
|
||||
const stylesForStatus = this.configuration?.statusStyles?.[status.label];
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class PollQuestionIndicator extends AbstractStatusIndicator {
|
||||
createIndicator() {
|
||||
const pollQuestionIndicator = this.openmct.indicators.simpleIndicator();
|
||||
|
||||
pollQuestionIndicator.text("No Poll Question");
|
||||
pollQuestionIndicator.text("Poll Question");
|
||||
pollQuestionIndicator.description("Set the current poll question");
|
||||
pollQuestionIndicator.iconClass('icon-status-poll-edit');
|
||||
pollQuestionIndicator.element.classList.add("c-indicator--operator-status");
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
connected = false;
|
||||
// stop listening for events
|
||||
couchEventSource.removeEventListener('message', self.onCouchMessage);
|
||||
couchEventSource.close();
|
||||
console.debug('🚪 Closed couch connection 🚪');
|
||||
|
||||
return;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<y-axis
|
||||
v-for="(yAxis, index) in yAxesIds"
|
||||
:id="yAxis.id"
|
||||
:key="`yAxis-${yAxis.id}-${index}`"
|
||||
:key="`yAxis-${index}`"
|
||||
:multiple-left-axes="multipleLeftAxes"
|
||||
:position="yAxis.id > 2 ? 'right' : 'left'"
|
||||
:class="{'plot-yaxis-right': yAxis.id > 2}"
|
||||
@@ -49,7 +49,6 @@
|
||||
:plot-left-tick-width="yAxis.id > 2 ? yAxis.tickWidth: plotLeftTickWidth"
|
||||
@yKeyChanged="setYAxisKey"
|
||||
@tickWidthChanged="onTickWidthChange"
|
||||
@toggleAxisVisibility="toggleSeriesForYAxis"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -92,11 +91,7 @@
|
||||
<mct-chart
|
||||
:rectangles="rectangles"
|
||||
:highlights="highlights"
|
||||
:annotated-points="annotatedPoints"
|
||||
:annotation-selections="annotationSelections"
|
||||
:show-limit-line-labels="showLimitLineLabels"
|
||||
:hidden-y-axis-ids="hiddenYAxisIds"
|
||||
:annotation-viewing-and-editing-allowed="annotationViewingAndEditingAllowed"
|
||||
@plotReinitializeCanvas="initCanvas"
|
||||
@chartLoaded="initialize"
|
||||
/>
|
||||
@@ -222,7 +217,6 @@ import MctTicks from "./MctTicks.vue";
|
||||
import MctChart from "./chart/MctChart.vue";
|
||||
import XAxis from "./axis/XAxis.vue";
|
||||
import YAxis from "./axis/YAxis.vue";
|
||||
import KDBush from 'kdbush';
|
||||
import _ from "lodash";
|
||||
|
||||
const OFFSET_THRESHOLD = 10;
|
||||
@@ -281,8 +275,6 @@ export default {
|
||||
return {
|
||||
altPressed: false,
|
||||
highlights: [],
|
||||
annotatedPoints: [],
|
||||
annotationSelections: [],
|
||||
lockHighlightPoint: false,
|
||||
yKeyOptions: [],
|
||||
yAxisLabel: '',
|
||||
@@ -300,9 +292,7 @@ export default {
|
||||
isFrozenOnMouseDown: false,
|
||||
cursorGuide: this.initCursorGuide,
|
||||
gridLines: this.initGridLines,
|
||||
yAxes: [],
|
||||
hiddenYAxisIds: [],
|
||||
yAxisListWithRange: []
|
||||
yAxes: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -333,10 +323,6 @@ export default {
|
||||
isFrozen() {
|
||||
return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
},
|
||||
annotationViewingAndEditingAllowed() {
|
||||
// only allow annotations viewing/editing if plot is paused or in fixed time mode
|
||||
return this.isFrozen || !this.isRealTime;
|
||||
},
|
||||
plotLegendPositionClass() {
|
||||
return !this.isNestedWithinAStackedPlot ? `plot-legend-${this.config.legend.get('position')}` : '';
|
||||
},
|
||||
@@ -379,7 +365,6 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.yAxisIdVisibility = {};
|
||||
this.offsetWidth = 0;
|
||||
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
@@ -406,8 +391,10 @@ export default {
|
||||
}));
|
||||
}
|
||||
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.$emit('configLoaded', configId);
|
||||
if (this.isNestedWithinAStackedPlot) {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.$emit('configLoaded', configId);
|
||||
}
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
@@ -422,83 +409,16 @@ export default {
|
||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
|
||||
|
||||
this.openmct.objectViews.on('clearData', this.clearData);
|
||||
this.$on('loadingUpdated', this.loadAnnotations);
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.setTimeContext();
|
||||
|
||||
this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes];
|
||||
|
||||
this.loaded = true;
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.removeEventListener('keyup', this.handleKeyUp);
|
||||
this.destroy();
|
||||
},
|
||||
methods: {
|
||||
updateSelection(selection) {
|
||||
const selectionContext = selection?.[0]?.[0]?.context?.item;
|
||||
if (!selectionContext
|
||||
|| this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)) {
|
||||
// Selection changed, but it's us, so ignoring it
|
||||
return;
|
||||
}
|
||||
|
||||
const selectionType = selection?.[0]?.[1]?.context?.type;
|
||||
if (selectionType !== 'plot-points-selection') {
|
||||
// wrong type of selection
|
||||
return;
|
||||
}
|
||||
|
||||
const currentXaxis = this.config.xAxis.get('displayRange');
|
||||
const currentYaxis = this.config.yAxis.get('displayRange');
|
||||
|
||||
// when there is no plot data, the ranges can be undefined
|
||||
// in which case we should not perform selection
|
||||
if (!currentXaxis || !currentYaxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedAnnotations = selection?.[0]?.[1]?.context?.annotations;
|
||||
if (selectedAnnotations?.length) {
|
||||
// just use first annotation
|
||||
const boundingBoxes = Object.values(selectedAnnotations[0].targets);
|
||||
let minX = Number.MAX_SAFE_INTEGER;
|
||||
let minY = Number.MAX_SAFE_INTEGER;
|
||||
let maxX = Number.MIN_SAFE_INTEGER;
|
||||
let maxY = Number.MIN_SAFE_INTEGER;
|
||||
boundingBoxes.forEach(boundingBox => {
|
||||
if (boundingBox.minX < minX) {
|
||||
minX = boundingBox.minX;
|
||||
}
|
||||
|
||||
if (boundingBox.maxX > maxX) {
|
||||
maxX = boundingBox.maxX;
|
||||
}
|
||||
|
||||
if (boundingBox.maxY > maxY) {
|
||||
maxY = boundingBox.maxY;
|
||||
}
|
||||
|
||||
if (boundingBox.minY < minY) {
|
||||
minY = boundingBox.minY;
|
||||
}
|
||||
});
|
||||
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: minX,
|
||||
max: maxX
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: minY,
|
||||
max: maxY
|
||||
});
|
||||
this.zoom('out', 0.2);
|
||||
}
|
||||
|
||||
this.prepareExistingAnnotationSelection(selectedAnnotations);
|
||||
},
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Alt') {
|
||||
this.altPressed = true;
|
||||
@@ -560,7 +480,6 @@ export default {
|
||||
this.listenTo(series, 'change:interpolate', () => {
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
this.listenTo(series, 'change:yAxisId', this.updateTicksAndSeriesForYAxis, this);
|
||||
|
||||
this.loadSeriesData(series);
|
||||
},
|
||||
@@ -572,32 +491,13 @@ export default {
|
||||
this.stopListening(plotSeries);
|
||||
},
|
||||
|
||||
updateTicksAndSeriesForYAxis(newAxisId, oldAxisId) {
|
||||
this.updateAxisUsageCount(oldAxisId, -1);
|
||||
this.updateAxisUsageCount(newAxisId, 1);
|
||||
},
|
||||
|
||||
updateAxisUsageCount(yAxisId, updateCountBy) {
|
||||
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy;
|
||||
}
|
||||
},
|
||||
async loadAnnotations() {
|
||||
if (!this.openmct.annotation.getAvailableTags().length) {
|
||||
// don't bother loading annotations if there are no tags
|
||||
return;
|
||||
}
|
||||
|
||||
const rawAnnotationsForPlot = [];
|
||||
await Promise.all(this.seriesModels.map(async (seriesModel) => {
|
||||
const seriesAnnotations = await this.openmct.annotation.getAnnotations(seriesModel.model.identifier);
|
||||
rawAnnotationsForPlot.push(...seriesAnnotations);
|
||||
}));
|
||||
if (rawAnnotationsForPlot) {
|
||||
this.annotatedPoints = this.findAnnotationPoints(rawAnnotationsForPlot);
|
||||
}
|
||||
},
|
||||
loadSeriesData(series) {
|
||||
//this check ensures that duplicate requests don't happen on load
|
||||
if (!this.timeContext) {
|
||||
@@ -621,7 +521,8 @@ export default {
|
||||
end: bounds.end
|
||||
};
|
||||
|
||||
series.load(options).then(this.stopLoading.bind(this));
|
||||
series.load(options)
|
||||
.then(this.stopLoading.bind(this));
|
||||
},
|
||||
|
||||
loadMoreData(range, purge) {
|
||||
@@ -813,91 +714,10 @@ export default {
|
||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
|
||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||
}
|
||||
},
|
||||
|
||||
marqueeAnnotations(annotationsToSelect) {
|
||||
annotationsToSelect.forEach(annotationToSelect => {
|
||||
Object.keys(annotationToSelect.targets).forEach(targetKeyString => {
|
||||
const target = annotationToSelect.targets[targetKeyString];
|
||||
const series = this.seriesModels.find(seriesModel => seriesModel.keyString === targetKeyString);
|
||||
if (!series) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yAxisId = series.get('yAxisId');
|
||||
const rectangle = {
|
||||
start: {
|
||||
x: target.minX,
|
||||
y: [target.minY],
|
||||
yAxisIds: [yAxisId]
|
||||
},
|
||||
end: {
|
||||
x: target.maxX,
|
||||
y: [target.maxY],
|
||||
yAxisIds: [yAxisId]
|
||||
},
|
||||
color: [1, 1, 1, 0.10]
|
||||
};
|
||||
this.rectangles.push(rectangle);
|
||||
});
|
||||
});
|
||||
},
|
||||
gatherNearbyAnnotations() {
|
||||
const nearbyAnnotations = [];
|
||||
this.config.series.models.forEach(series => {
|
||||
if (series.closest.annotationsById) {
|
||||
Object.values(series.closest.annotationsById).forEach(closeAnnotation => {
|
||||
const addedAnnotationAlready = nearbyAnnotations.some(annotation => {
|
||||
return _.isEqual(annotation.targets, closeAnnotation.targets)
|
||||
&& _.isEqual(annotation.tags, closeAnnotation.tags);
|
||||
});
|
||||
if (!addedAnnotationAlready) {
|
||||
nearbyAnnotations.push(closeAnnotation);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return nearbyAnnotations;
|
||||
},
|
||||
|
||||
prepareExistingAnnotationSelection(annotations) {
|
||||
const targetDomainObjects = {};
|
||||
this.config.series.models.forEach(series => {
|
||||
targetDomainObjects[series.keyString] = series.domainObject;
|
||||
});
|
||||
|
||||
const targetDetails = {};
|
||||
const uniqueBoundsAnnotations = [];
|
||||
annotations.forEach(annotation => {
|
||||
Object.entries(annotation.targets).forEach(([key, value]) => {
|
||||
targetDetails[key] = value;
|
||||
});
|
||||
|
||||
const boundingBoxAlreadyAdded = uniqueBoundsAnnotations.some(existingAnnotation => {
|
||||
const existingBoundingBox = Object.values(existingAnnotation.targets)[0];
|
||||
const newBoundingBox = Object.values(annotation.targets)[0];
|
||||
|
||||
return (existingBoundingBox.minX === newBoundingBox.minX
|
||||
&& existingBoundingBox.minY === newBoundingBox.minY
|
||||
&& existingBoundingBox.maxX === newBoundingBox.maxX
|
||||
&& existingBoundingBox.maxY === newBoundingBox.maxY);
|
||||
|
||||
});
|
||||
if (!boundingBoxAlreadyAdded) {
|
||||
uniqueBoundsAnnotations.push(annotation);
|
||||
}
|
||||
});
|
||||
this.marqueeAnnotations(uniqueBoundsAnnotations);
|
||||
|
||||
return {
|
||||
targetDomainObjects,
|
||||
targetDetails
|
||||
};
|
||||
},
|
||||
initialize() {
|
||||
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
|
||||
this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);
|
||||
@@ -905,13 +725,8 @@ export default {
|
||||
|
||||
// Setup canvas etc.
|
||||
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
|
||||
this.yScale = [];
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
this.yScale.push({
|
||||
id: yAxis.id,
|
||||
scale: new LinearScale(yAxis.get('displayRange'))
|
||||
});
|
||||
});
|
||||
//TODO: handle yScale, zoom/pan for all yAxes
|
||||
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
|
||||
|
||||
this.pan = undefined;
|
||||
this.marquee = undefined;
|
||||
@@ -927,8 +742,9 @@ export default {
|
||||
this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal;
|
||||
|
||||
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange.bind(this, yAxis.id), this);
|
||||
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange, this);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -938,11 +754,9 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
onYAxisChange(yAxisId, displayBounds) {
|
||||
onYAxisChange(displayBounds) {
|
||||
if (displayBounds) {
|
||||
this.yScale.filter((yAxis) => yAxis.id === yAxisId).forEach((yAxis) => {
|
||||
yAxis.scale.domain(displayBounds);
|
||||
});
|
||||
this.yScale.domain(displayBounds);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -966,30 +780,15 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
toggleSeriesForYAxis({ id, visible}) {
|
||||
//if toggling to visible, re-fetch the data for the series that are part of this y Axis
|
||||
if (visible === true) {
|
||||
this.config.series.models.filter(model => model.get('yAxisId') === id)
|
||||
.forEach(this.loadSeriesData, this);
|
||||
}
|
||||
|
||||
this.yAxisIdVisibility[id] = visible;
|
||||
this.hiddenYAxisIds = Object.keys(this.yAxisIdVisibility).map(Number).filter(key => {
|
||||
return this.yAxisIdVisibility[key] === false;
|
||||
});
|
||||
},
|
||||
|
||||
trackMousePosition(event) {
|
||||
this.trackChartElementBounds(event);
|
||||
this.xScale.range({
|
||||
min: 0,
|
||||
max: this.chartElementBounds.width
|
||||
});
|
||||
this.yScale.forEach((yAxis) => {
|
||||
yAxis.scale.range({
|
||||
min: 0,
|
||||
max: this.chartElementBounds.height
|
||||
});
|
||||
this.yScale.range({
|
||||
min: 0,
|
||||
max: this.chartElementBounds.height
|
||||
});
|
||||
|
||||
this.positionOverElement = {
|
||||
@@ -998,13 +797,9 @@ export default {
|
||||
- (event.clientY - this.chartElementBounds.top)
|
||||
};
|
||||
|
||||
const yLocationForPositionOverPlot = this.yScale.map((yAxis) => yAxis.scale.invert(this.positionOverElement.y));
|
||||
const yAxisIds = this.yScale.map((yAxis) => yAxis.id);
|
||||
// Also store the order of yAxisIds so that we can associate the y location to the yAxis
|
||||
this.positionOverPlot = {
|
||||
x: this.xScale.invert(this.positionOverElement.x),
|
||||
y: yLocationForPositionOverPlot,
|
||||
yAxisIds
|
||||
y: this.yScale.invert(this.positionOverElement.y)
|
||||
};
|
||||
|
||||
if (this.cursorGuide) {
|
||||
@@ -1017,12 +812,6 @@ export default {
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
getYPositionForYAxis(object, yAxis) {
|
||||
const index = object.yAxisIds.findIndex(yAxisId => yAxisId === yAxis.get('id'));
|
||||
|
||||
return object.y[index];
|
||||
},
|
||||
|
||||
updateCrosshairs(event) {
|
||||
this.cursorGuideVertical.style.left = (event.clientX - this.chartElementBounds.x) + 'px';
|
||||
this.cursorGuideHorizontal.style.top = (event.clientY - this.chartElementBounds.y) + 'px';
|
||||
@@ -1076,7 +865,7 @@ export default {
|
||||
},
|
||||
|
||||
onMouseDown(event) {
|
||||
// do not monitor drag events on browser context click
|
||||
// do not monitor drag events on browser context click
|
||||
if (event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
@@ -1088,12 +877,10 @@ export default {
|
||||
const isFrozen = this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
this.isFrozenOnMouseDown = isFrozen;
|
||||
|
||||
if (event.altKey && !event.shiftKey) {
|
||||
if (event.altKey) {
|
||||
return this.startPan(event);
|
||||
} else if (this.annotationViewingAndEditingAllowed && event.altKey && event.shiftKey) {
|
||||
return this.startMarquee(event, true);
|
||||
} else {
|
||||
return this.startMarquee(event, false);
|
||||
return this.startMarquee(event);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1101,7 +888,7 @@ export default {
|
||||
this.stopListening(window, 'mouseup', this.onMouseUp, this);
|
||||
this.stopListening(window, 'mousemove', this.trackMousePosition, this);
|
||||
|
||||
if (this.isMouseClick() && event.shiftKey) {
|
||||
if (this.isMouseClick()) {
|
||||
this.lockHighlightPoint = !this.lockHighlightPoint;
|
||||
this.$emit('lockHighlightPoint', this.lockHighlightPoint);
|
||||
}
|
||||
@@ -1129,9 +916,8 @@ export default {
|
||||
}
|
||||
|
||||
const { start, end } = this.marquee;
|
||||
const someYPositionOverPlot = start.y.some(y => y);
|
||||
|
||||
return start.x === end.x && someYPositionOverPlot;
|
||||
return start.x === end.x && start.y === end.y;
|
||||
},
|
||||
|
||||
updateMarquee() {
|
||||
@@ -1143,9 +929,7 @@ export default {
|
||||
this.marquee.endPixels = this.positionOverElement;
|
||||
},
|
||||
|
||||
startMarquee(event, annotationEvent) {
|
||||
this.rectangles = [];
|
||||
this.annotationSelections = [];
|
||||
startMarquee(event) {
|
||||
this.canvas.classList.remove('plot-drag');
|
||||
this.canvas.classList.add('plot-marquee');
|
||||
|
||||
@@ -1159,181 +943,12 @@ export default {
|
||||
end: this.positionOverPlot,
|
||||
color: [1, 1, 1, 0.5]
|
||||
};
|
||||
if (annotationEvent) {
|
||||
this.marquee.annotationEvent = true;
|
||||
}
|
||||
|
||||
this.rectangles.push(this.marquee);
|
||||
this.trackHistory();
|
||||
}
|
||||
},
|
||||
selectNearbyAnnotations(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!this.annotationViewingAndEditingAllowed || this.annotationSelections.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nearbyAnnotations = this.gatherNearbyAnnotations();
|
||||
const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations);
|
||||
this.selectPlotAnnotations({
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations: nearbyAnnotations
|
||||
});
|
||||
},
|
||||
selectPlotAnnotations({targetDetails, targetDomainObjects, annotations}) {
|
||||
const selection =
|
||||
[
|
||||
{
|
||||
element: this.openmct.layout.$refs.browseObject.$el,
|
||||
context: {
|
||||
item: this.domainObject
|
||||
}
|
||||
},
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
type: 'plot-points-selection',
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations,
|
||||
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
|
||||
onAnnotationChange: this.onAnnotationChange
|
||||
}
|
||||
}
|
||||
];
|
||||
this.openmct.selection.select(selection, true);
|
||||
},
|
||||
selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBox, event) {
|
||||
let targetDomainObjects = {};
|
||||
let targetDetails = {};
|
||||
let annotations = {};
|
||||
pointsInBox.forEach(pointInBox => {
|
||||
if (pointInBox.length) {
|
||||
const seriesID = pointInBox[0].series.keyString;
|
||||
const boundingBoxWithId = boundingBoxPerYAxis.find(box => box.id === pointInBox[0].series.get('yAxisId'));
|
||||
targetDetails[seriesID] = boundingBoxWithId?.boundingBox;
|
||||
|
||||
targetDomainObjects[seriesID] = pointInBox[0].series.domainObject;
|
||||
}
|
||||
});
|
||||
this.selectPlotAnnotations({
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations
|
||||
});
|
||||
},
|
||||
findAnnotationPoints(rawAnnotations) {
|
||||
const annotationsByPoints = [];
|
||||
rawAnnotations.forEach(rawAnnotation => {
|
||||
if (rawAnnotation.targets) {
|
||||
const targetValues = Object.values(rawAnnotation.targets);
|
||||
const targetKeys = Object.keys(rawAnnotation.targets);
|
||||
if (targetValues && targetValues.length) {
|
||||
let boundingBoxPerYAxis = [];
|
||||
targetValues.forEach((boundingBox, index) => {
|
||||
const seriesId = targetKeys[index];
|
||||
const series = this.seriesModels.find(seriesModel => seriesModel.keyString === seriesId);
|
||||
if (!series) {
|
||||
return;
|
||||
}
|
||||
|
||||
boundingBoxPerYAxis.push({
|
||||
id: series.get('yAxisId'),
|
||||
boundingBox
|
||||
});
|
||||
});
|
||||
|
||||
const pointsInBox = this.getPointsInBox(boundingBoxPerYAxis, rawAnnotation);
|
||||
if (pointsInBox && pointsInBox.length) {
|
||||
annotationsByPoints.push(pointsInBox.flat());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return annotationsByPoints.flat();
|
||||
},
|
||||
getPointsInBox(boundingBoxPerYAxis, rawAnnotation) {
|
||||
// load series models in KD-Trees
|
||||
const seriesKDTrees = [];
|
||||
this.seriesModels.forEach(seriesModel => {
|
||||
const boundingBoxWithId = boundingBoxPerYAxis.find(box => box.id === seriesModel.get('yAxisId'));
|
||||
const boundingBox = boundingBoxWithId?.boundingBox;
|
||||
//Series was probably added after the last annotations were saved
|
||||
if (!boundingBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
const seriesData = seriesModel.getSeriesData();
|
||||
if (seriesData && seriesData.length) {
|
||||
const kdTree = new KDBush(seriesData,
|
||||
(point) => {
|
||||
return seriesModel.getXVal(point);
|
||||
},
|
||||
(point) => {
|
||||
return seriesModel.getYVal(point);
|
||||
}
|
||||
);
|
||||
const searchResults = [];
|
||||
const rangeResults = kdTree.range(boundingBox.minX, boundingBox.minY, boundingBox.maxX, boundingBox.maxY);
|
||||
rangeResults.forEach(id => {
|
||||
const seriesDatum = seriesData[id];
|
||||
if (seriesDatum) {
|
||||
const result = {
|
||||
series: seriesModel,
|
||||
point: seriesDatum
|
||||
};
|
||||
searchResults.push(result);
|
||||
}
|
||||
|
||||
if (rawAnnotation) {
|
||||
if (!seriesDatum.annotationsById) {
|
||||
seriesDatum.annotationsById = {};
|
||||
}
|
||||
|
||||
const annotationKeyString = this.openmct.objects.makeKeyString(rawAnnotation.identifier);
|
||||
seriesDatum.annotationsById[annotationKeyString] = rawAnnotation;
|
||||
}
|
||||
|
||||
});
|
||||
if (searchResults.length) {
|
||||
seriesKDTrees.push(searchResults);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return seriesKDTrees;
|
||||
},
|
||||
endAnnotationMarquee(event) {
|
||||
const boundingBoxPerYAxis = [];
|
||||
this.yAxisListWithRange.forEach((yAxis, yIndex) => {
|
||||
const minX = Math.min(this.marquee.start.x, this.marquee.end.x);
|
||||
const minY = Math.min(this.marquee.start.y[yIndex], this.marquee.end.y[yIndex]);
|
||||
const maxX = Math.max(this.marquee.start.x, this.marquee.end.x);
|
||||
const maxY = Math.max(this.marquee.start.y[yIndex], this.marquee.end.y[yIndex]);
|
||||
const boundingBox = {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY
|
||||
};
|
||||
boundingBoxPerYAxis.push({
|
||||
id: yAxis.get('id'),
|
||||
boundingBox
|
||||
});
|
||||
});
|
||||
|
||||
const pointsInBox = this.getPointsInBox(boundingBoxPerYAxis);
|
||||
if (!pointsInBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.annotationSelections = pointsInBox.flat();
|
||||
this.selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBox, event);
|
||||
},
|
||||
endZoomMarquee() {
|
||||
endMarquee() {
|
||||
const startPixels = this.marquee.startPixels;
|
||||
const endPixels = this.marquee.endPixels;
|
||||
const marqueeDistance = Math.sqrt(
|
||||
@@ -1346,13 +961,9 @@ export default {
|
||||
min: Math.min(this.marquee.start.x, this.marquee.end.x),
|
||||
max: Math.max(this.marquee.start.x, this.marquee.end.x)
|
||||
});
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
const yStartPosition = this.getYPositionForYAxis(this.marquee.start, yAxis);
|
||||
const yEndPosition = this.getYPositionForYAxis(this.marquee.end, yAxis);
|
||||
yAxis.set('displayRange', {
|
||||
min: Math.min(yStartPosition, yEndPosition),
|
||||
max: Math.max(yStartPosition, yEndPosition)
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: Math.min(this.marquee.start.y, this.marquee.end.y),
|
||||
max: Math.max(this.marquee.start.y, this.marquee.end.y)
|
||||
});
|
||||
this.userViewportChangeEnd();
|
||||
} else {
|
||||
@@ -1360,40 +971,18 @@ export default {
|
||||
// if marquee zoom doesn't occur.
|
||||
this.plotHistory.pop();
|
||||
}
|
||||
},
|
||||
endMarquee(event) {
|
||||
if (this.marquee.annotationEvent) {
|
||||
this.endAnnotationMarquee(event);
|
||||
} else {
|
||||
this.endZoomMarquee();
|
||||
this.rectangles = [];
|
||||
}
|
||||
|
||||
this.marquee = null;
|
||||
},
|
||||
|
||||
onAnnotationChange(annotations) {
|
||||
if (this.marquee) {
|
||||
this.marquee.annotationEvent = false;
|
||||
this.endMarquee();
|
||||
}
|
||||
|
||||
this.loadAnnotations();
|
||||
this.rectangles = [];
|
||||
this.marquee = undefined;
|
||||
},
|
||||
|
||||
zoom(zoomDirection, zoomFactor) {
|
||||
const currentXaxis = this.config.xAxis.get('displayRange');
|
||||
|
||||
let doesYAxisHaveRange = false;
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
if (yAxisModel.get('displayRange')) {
|
||||
doesYAxisHaveRange = true;
|
||||
}
|
||||
});
|
||||
const currentYaxis = this.config.yAxis.get('displayRange');
|
||||
|
||||
// when there is no plot data, the ranges can be undefined
|
||||
// in which case we should not perform zoom
|
||||
if (!currentXaxis || !doesYAxisHaveRange) {
|
||||
if (!currentXaxis || !currentYaxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1401,6 +990,7 @@ export default {
|
||||
this.trackHistory();
|
||||
|
||||
const xAxisDist = (currentXaxis.max - currentXaxis.min) * zoomFactor;
|
||||
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
|
||||
|
||||
if (zoomDirection === 'in') {
|
||||
this.config.xAxis.set('displayRange', {
|
||||
@@ -1408,17 +998,9 @@ export default {
|
||||
max: currentXaxis.max - xAxisDist
|
||||
});
|
||||
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
const currentYaxis = yAxisModel.get('displayRange');
|
||||
if (!currentYaxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
|
||||
yAxisModel.set('displayRange', {
|
||||
min: currentYaxis.min + yAxisDist,
|
||||
max: currentYaxis.max - yAxisDist
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: currentYaxis.min + yAxisDist,
|
||||
max: currentYaxis.max - yAxisDist
|
||||
});
|
||||
} else if (zoomDirection === 'out') {
|
||||
this.config.xAxis.set('displayRange', {
|
||||
@@ -1426,17 +1008,9 @@ export default {
|
||||
max: currentXaxis.max + xAxisDist
|
||||
});
|
||||
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
const currentYaxis = yAxisModel.get('displayRange');
|
||||
if (!currentYaxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
|
||||
yAxisModel.set('displayRange', {
|
||||
min: currentYaxis.min - yAxisDist,
|
||||
max: currentYaxis.max + yAxisDist
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: currentYaxis.min - yAxisDist,
|
||||
max: currentYaxis.max + yAxisDist
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1453,17 +1027,11 @@ export default {
|
||||
}
|
||||
|
||||
let xDisplayRange = this.config.xAxis.get('displayRange');
|
||||
|
||||
let doesYAxisHaveRange = false;
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
if (yAxisModel.get('displayRange')) {
|
||||
doesYAxisHaveRange = true;
|
||||
}
|
||||
});
|
||||
let yDisplayRange = this.config.yAxis.get('displayRange');
|
||||
|
||||
// when there is no plot data, the ranges can be undefined
|
||||
// in which case we should not perform zoom
|
||||
if (!xDisplayRange || !doesYAxisHaveRange) {
|
||||
if (!xDisplayRange || !yDisplayRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1471,19 +1039,22 @@ export default {
|
||||
window.clearTimeout(this.stillZooming);
|
||||
|
||||
let xAxisDist = (xDisplayRange.max - xDisplayRange.min);
|
||||
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
|
||||
let xDistMouseToMax = xDisplayRange.max - this.positionOverPlot.x;
|
||||
let xDistMouseToMin = this.positionOverPlot.x - xDisplayRange.min;
|
||||
let yDistMouseToMax = yDisplayRange.max - this.positionOverPlot.y;
|
||||
let yDistMouseToMin = this.positionOverPlot.y - yDisplayRange.min;
|
||||
let xAxisMaxDist = xDistMouseToMax / xAxisDist;
|
||||
let xAxisMinDist = xDistMouseToMin / xAxisDist;
|
||||
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
|
||||
let yAxisMinDist = yDistMouseToMin / yAxisDist;
|
||||
|
||||
let plotHistoryStep;
|
||||
|
||||
if (!plotHistoryStep) {
|
||||
const yRangeList = [];
|
||||
this.yAxisListWithRange.map((yAxis) => yRangeList.push(yAxis.get('displayRange')));
|
||||
plotHistoryStep = {
|
||||
x: this.config.xAxis.get('displayRange'),
|
||||
y: yRangeList
|
||||
x: xDisplayRange,
|
||||
y: yDisplayRange
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1494,47 +1065,20 @@ export default {
|
||||
max: xDisplayRange.max - ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
|
||||
});
|
||||
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
const yDisplayRange = yAxisModel.get('displayRange');
|
||||
if (!yDisplayRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel);
|
||||
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
|
||||
let yDistMouseToMax = yDisplayRange.max - yPosition;
|
||||
let yDistMouseToMin = yPosition - yDisplayRange.min;
|
||||
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
|
||||
let yAxisMinDist = yDistMouseToMin / yAxisDist;
|
||||
|
||||
yAxisModel.set('displayRange', {
|
||||
min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
|
||||
max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
|
||||
max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
|
||||
});
|
||||
} else if (event.wheelDelta >= 0) {
|
||||
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: xDisplayRange.min - ((xAxisDist * ZOOM_AMT) * xAxisMinDist),
|
||||
max: xDisplayRange.max + ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
|
||||
});
|
||||
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
const yDisplayRange = yAxisModel.get('displayRange');
|
||||
if (!yDisplayRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel);
|
||||
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
|
||||
let yDistMouseToMax = yDisplayRange.max - yPosition;
|
||||
let yDistMouseToMin = yPosition - yDisplayRange.min;
|
||||
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
|
||||
let yAxisMinDist = yDistMouseToMin / yAxisDist;
|
||||
|
||||
yAxisModel.set('displayRange', {
|
||||
min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
|
||||
max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
|
||||
max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1567,48 +1111,24 @@ export default {
|
||||
}
|
||||
|
||||
const dX = this.pan.start.x - this.positionOverPlot.x;
|
||||
const dY = this.pan.start.y - this.positionOverPlot.y;
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: xRange.min + dX,
|
||||
max: xRange.max + dX
|
||||
});
|
||||
|
||||
const dY = [];
|
||||
this.positionOverPlot.y.forEach((yAxisPosition, index) => {
|
||||
const yAxisId = this.positionOverPlot.yAxisIds[index];
|
||||
dY.push({
|
||||
yAxisId: yAxisId,
|
||||
y: this.pan.start.y[index] - yAxisPosition
|
||||
});
|
||||
});
|
||||
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
const yRange = yAxis.get('displayRange');
|
||||
if (!yRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yIndex = dY.findIndex(y => y.yAxisId === yAxis.get('id'));
|
||||
|
||||
yAxis.set('displayRange', {
|
||||
min: yRange.min + dY[yIndex].y,
|
||||
max: yRange.max + dY[yIndex].y
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: yRange.min + dY,
|
||||
max: yRange.max + dY
|
||||
});
|
||||
},
|
||||
|
||||
trackHistory() {
|
||||
const yRangeList = [];
|
||||
const yAxisIds = [];
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
yRangeList.push(yAxis.get('displayRange'));
|
||||
yAxisIds.push(yAxis.get('id'));
|
||||
});
|
||||
this.plotHistory.push({
|
||||
x: this.config.xAxis.get('displayRange'),
|
||||
y: yRangeList,
|
||||
yAxisIds
|
||||
y: this.config.yAxis.get('displayRange')
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1618,9 +1138,7 @@ export default {
|
||||
},
|
||||
|
||||
freeze() {
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
yAxis.set('frozen', true);
|
||||
});
|
||||
this.config.yAxis.set('frozen', true);
|
||||
this.config.xAxis.set('frozen', true);
|
||||
this.setStatus();
|
||||
},
|
||||
@@ -1631,9 +1149,7 @@ export default {
|
||||
},
|
||||
|
||||
clearPanZoomHistory() {
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
yAxis.set('frozen', false);
|
||||
});
|
||||
this.config.yAxis.set('frozen', false);
|
||||
this.config.xAxis.set('frozen', false);
|
||||
this.setStatus();
|
||||
this.plotHistory = [];
|
||||
@@ -1648,11 +1164,7 @@ export default {
|
||||
}
|
||||
|
||||
this.config.xAxis.set('displayRange', previousAxisRanges.x);
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
const yPosition = this.getYPositionForYAxis(previousAxisRanges, yAxis);
|
||||
yAxis.set('displayRange', yPosition);
|
||||
});
|
||||
|
||||
this.config.yAxis.set('displayRange', previousAxisRanges.y);
|
||||
this.userViewportChangeEnd();
|
||||
},
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
<div
|
||||
ref="plotWrapper"
|
||||
class="c-plot holder holder-plot has-control-bar"
|
||||
:class="staleClass"
|
||||
>
|
||||
<div
|
||||
ref="plotContainer"
|
||||
@@ -51,7 +50,6 @@ import eventHelpers from './lib/eventHelpers';
|
||||
import ImageExporter from '../../exporters/ImageExporter';
|
||||
import MctPlot from './MctPlot.vue';
|
||||
import ProgressBar from "../../ui/components/ProgressBar.vue";
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -76,95 +74,21 @@ export default {
|
||||
cursorGuide: false,
|
||||
gridLines: !this.options.compact,
|
||||
loading: false,
|
||||
status: '',
|
||||
staleObjects: []
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
staleClass() {
|
||||
if (this.staleObjects.length !== 0) {
|
||||
return 'is-stale';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
this.loadComposition();
|
||||
this.stalenessSubscription = {};
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroy();
|
||||
},
|
||||
methods: {
|
||||
loadComposition() {
|
||||
this.compositionCollection = this.openmct.composition.get(this.domainObject);
|
||||
|
||||
if (this.compositionCollection) {
|
||||
this.compositionCollection.on('add', this.addItem);
|
||||
this.compositionCollection.on('remove', this.removeItem);
|
||||
this.compositionCollection.load();
|
||||
}
|
||||
|
||||
},
|
||||
addItem(object) {
|
||||
const keystring = this.openmct.objects.makeKeyString(object.identifier);
|
||||
|
||||
if (!this.stalenessSubscription[keystring]) {
|
||||
this.stalenessSubscription[keystring] = {};
|
||||
this.stalenessSubscription[keystring].stalenessUtils = new StalenessUtils(this.openmct, object);
|
||||
}
|
||||
|
||||
this.openmct.telemetry.isStale(object).then((stalenessResponse) => {
|
||||
if (stalenessResponse !== undefined) {
|
||||
this.handleStaleness(keystring, stalenessResponse);
|
||||
}
|
||||
});
|
||||
const unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness(object, (stalenessResponse) => {
|
||||
this.handleStaleness(keystring, stalenessResponse);
|
||||
});
|
||||
|
||||
this.stalenessSubscription[keystring].unsubscribe = unsubscribeFromStaleness;
|
||||
},
|
||||
removeItem(object) {
|
||||
const SKIP_CHECK = true;
|
||||
const keystring = this.openmct.objects.makeKeyString(object);
|
||||
this.stalenessSubscription[keystring].unsubscribe();
|
||||
this.stalenessSubscription[keystring].stalenessUtils.destroy();
|
||||
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
|
||||
},
|
||||
handleStaleness(id, stalenessResponse, skipCheck = false) {
|
||||
if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse, id)) {
|
||||
const index = this.staleObjects.indexOf(id);
|
||||
if (stalenessResponse.isStale) {
|
||||
if (index === -1) {
|
||||
this.staleObjects.push(id);
|
||||
}
|
||||
} else {
|
||||
if (index !== -1) {
|
||||
this.staleObjects.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
loadingUpdated(loading) {
|
||||
this.loading = loading;
|
||||
},
|
||||
destroy() {
|
||||
if (this.stalenessSubscription) {
|
||||
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
|
||||
stalenessSubscription.unsubscribe();
|
||||
stalenessSubscription.stalenessUtils.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.compositionCollection) {
|
||||
this.compositionCollection.off('add', this.addItem);
|
||||
this.compositionCollection.off('remove', this.removeItem);
|
||||
}
|
||||
|
||||
this.stopListening();
|
||||
},
|
||||
exportJPG() {
|
||||
|
||||