Compare commits
30 Commits
test-form-
...
fix-openmc
Author | SHA1 | Date | |
---|---|---|---|
3e89726291 | |||
7765e34c42 | |||
dd2fe14c8e | |||
d421a44426 | |||
08db862a2a | |||
c6b300a6be | |||
b4740f2f7f | |||
d20c2a3e3c | |||
8d1a2e6716 | |||
01f724959d | |||
3ae6290ec3 | |||
ba5ed27e74 | |||
ca737d8afa | |||
33a275e8bc | |||
60e808689c | |||
8847c862fa | |||
1b71a3bf33 | |||
9980aab18f | |||
5e530aa625 | |||
986c596d90 | |||
4d84b16d8b | |||
20c7b23a4f | |||
d1c7d133fc | |||
edbbebe329 | |||
f98a2cdd6b | |||
22621aaaf8 | |||
e0ca6200bb | |||
70074c52c8 | |||
d5adaf6e8c | |||
8125632728 |
@ -13,8 +13,8 @@ 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");
|
||||
const projectRootDir = path.resolve(__dirname, "..");
|
||||
let gitRevision = "error-retrieving-revision";
|
||||
let gitBranch = "error-retrieving-branch";
|
||||
|
||||
@ -23,7 +23,7 @@ try {
|
||||
.execSync("git rev-parse HEAD")
|
||||
.toString()
|
||||
.trim();
|
||||
gitBranch = require("child_process")
|
||||
gitBranch = require("child_process")
|
||||
.execSync("git rev-parse --abbrev-ref HEAD")
|
||||
.toString()
|
||||
.trim();
|
||||
@ -31,7 +31,6 @@ try {
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
const projectRootDir = path.resolve(__dirname, "..");
|
||||
|
||||
/** @type {import('webpack').Configuration} */
|
||||
const config = {
|
||||
@ -80,6 +79,7 @@ const config = {
|
||||
projectRootDir,
|
||||
"src/api/objects/object-utils.js"
|
||||
),
|
||||
"kdbush": path.join(projectRootDir, "node_modules/kdbush/kdbush.min.js"),
|
||||
utils: path.join(projectRootDir, "src/utils")
|
||||
}
|
||||
},
|
||||
@ -88,7 +88,7 @@ const config = {
|
||||
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
|
||||
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
|
||||
__OPENMCT_REVISION__: `'${gitRevision}'`,
|
||||
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`
|
||||
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
@ -163,12 +163,11 @@ const config = {
|
||||
}
|
||||
]
|
||||
},
|
||||
stats: "errors-warnings",
|
||||
performance: {
|
||||
// We should eventually consider chunking to decrease
|
||||
// these values
|
||||
maxEntrypointSize: 25000000,
|
||||
maxAssetSize: 25000000
|
||||
maxEntrypointSize: 27000000,
|
||||
maxAssetSize: 27000000
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -144,7 +144,9 @@ async function createNotification(page, createNotificationOptions) {
|
||||
* @param {string} name
|
||||
*/
|
||||
async function expandTreePaneItemByName(page, name) {
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
|
||||
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
|
||||
await expandTriangle.click();
|
||||
@ -218,6 +220,24 @@ async function openObjectTreeContextMenu(page, url) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the entire object tree (every expandable tree item).
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {"Main Tree" | "Create Modal Tree"} [treeName="Main Tree"]
|
||||
*/
|
||||
async function expandEntireTree(page, treeName = "Main Tree") {
|
||||
const treeLocator = page.getByRole('tree', {
|
||||
name: treeName
|
||||
});
|
||||
const collapsedTreeItems = treeLocator.getByRole('treeitem', {
|
||||
expanded: false
|
||||
}).locator('span.c-disclosure-triangle.is-enabled');
|
||||
|
||||
while (await collapsedTreeItems.count() > 0) {
|
||||
await collapsedTreeItems.nth(0).click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the UUID of the currently focused object by parsing the current URL
|
||||
* and returning the last UUID in the path.
|
||||
@ -362,6 +382,7 @@ module.exports = {
|
||||
createDomainObjectWithDefaults,
|
||||
createNotification,
|
||||
expandTreePaneItemByName,
|
||||
expandEntireTree,
|
||||
createPlanFromJSON,
|
||||
openObjectTreeContextMenu,
|
||||
getHashUrlToDomainObject,
|
||||
|
27
e2e/helper/addInitExampleUser.js
Normal file
@ -0,0 +1,27 @@
|
||||
/*****************************************************************************
|
||||
* 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());
|
||||
});
|
76
e2e/helper/addInitFileInputObject.js
Normal file
@ -0,0 +1,76 @@
|
||||
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));
|
||||
});
|
27
e2e/helper/addInitOperatorStatus.js
Normal file
@ -0,0 +1,27 @@
|
||||
/*****************************************************************************
|
||||
* 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());
|
||||
});
|
BIN
e2e/test-data/rick.jpg
Normal file
After Width: | Height: | Size: 10 KiB |
@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
const { createDomainObjectWithDefaults, createNotification } = require('../../appActions.js');
|
||||
const { createDomainObjectWithDefaults, createNotification, expandEntireTree } = 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, { waitUntil: 'networkidle' });
|
||||
await page.goto(timer1.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name);
|
||||
await page.goto(timer2.url, { waitUntil: 'networkidle' });
|
||||
await page.goto(timer2.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name);
|
||||
await page.goto(timer3.url, { waitUntil: 'networkidle' });
|
||||
await page.goto(timer3.url);
|
||||
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, { waitUntil: 'networkidle' });
|
||||
await page.goto(folder1.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name);
|
||||
await page.goto(folder2.url, { waitUntil: 'networkidle' });
|
||||
await page.goto(folder2.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name);
|
||||
await page.goto(folder3.url, { waitUntil: 'networkidle' });
|
||||
await page.goto(folder3.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name);
|
||||
|
||||
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
|
||||
@ -109,4 +109,57 @@ test.describe('AppActions', () => {
|
||||
await expect(page.locator('.c-message-banner')).toHaveClass(/error/);
|
||||
await page.locator('[aria-label="Dismiss"]').click();
|
||||
});
|
||||
test('expandEntireTree', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const rootFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
});
|
||||
const folder1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
parent: rootFolder.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
parent: folder1.uuid
|
||||
});
|
||||
const folder2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
parent: folder1.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
parent: folder1.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
parent: folder2.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
parent: folder2.uuid
|
||||
});
|
||||
|
||||
await page.goto('./#/browse/mine');
|
||||
await expandEntireTree(page);
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: "Main Tree"
|
||||
});
|
||||
const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false });
|
||||
expect(await treePaneCollapsedItems.count()).toBe(0);
|
||||
|
||||
await page.goto('./#/browse/mine');
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click the object specified by 'type'
|
||||
await page.click(`li[role='menuitem']:text("Clock")`);
|
||||
await expandEntireTree(page, "Create Modal Tree");
|
||||
const locatorTree = page.getByRole("tree", {
|
||||
name: "Create Modal Tree"
|
||||
});
|
||||
const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]');
|
||||
expect(await locatorTreeCollapsedItems.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
@ -30,6 +30,8 @@ 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 }) => {
|
||||
@ -68,6 +70,41 @@ 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,48 +43,80 @@ test.describe('Move & link item tests', () => {
|
||||
name: 'Child Folder',
|
||||
parent: parentFolder.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const grandchildFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Grandchild Folder',
|
||||
parent: childFolder.uuid
|
||||
});
|
||||
|
||||
// Attempt to move 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();
|
||||
await page.locator('button[title="Show selected item in tree"]').click();
|
||||
|
||||
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: 'Parent Folder'
|
||||
}).click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
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 page.getByRole('menuitem', {
|
||||
name: /Move/
|
||||
}).click();
|
||||
|
||||
const createModalTree = page.getByRole('tree', {
|
||||
name: "Create Modal Tree"
|
||||
});
|
||||
const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
|
||||
const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
|
||||
name: parentFolder.name
|
||||
});
|
||||
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
|
||||
|
||||
const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
});
|
||||
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await childFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
|
||||
|
||||
const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
|
||||
name: grandchildFolder.name
|
||||
});
|
||||
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await grandchildFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
|
||||
await parentFolderLocatorTreeItem.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 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({
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
}).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Move/
|
||||
}).click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
const myItemsPaneTreeItem = treePane.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
|
||||
// Expect that Child Folder is in My Items, the root folder
|
||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
|
||||
expect(myItemsPaneTreeItem.locator('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;
|
||||
@ -114,7 +146,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 = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButton = page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled = await okButton.isDisabled();
|
||||
expect.soft(okButtonStateDisabled).toBeTruthy();
|
||||
|
||||
@ -138,7 +170,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 = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButton2 = page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled2 = await okButton2.isDisabled();
|
||||
expect(okButtonStateDisabled2).toBeTruthy();
|
||||
});
|
||||
@ -158,48 +190,80 @@ test.describe('Move & link item tests', () => {
|
||||
name: 'Child Folder',
|
||||
parent: parentFolder.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const grandchildFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Grandchild Folder',
|
||||
parent: childFolder.uuid
|
||||
});
|
||||
|
||||
// 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();
|
||||
// Attempt to move parent to its own grandparent
|
||||
await page.locator('button[title="Show selected item in tree"]').click();
|
||||
|
||||
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: 'Parent Folder'
|
||||
}).click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
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 page.getByRole('menuitem', {
|
||||
name: /Move/
|
||||
}).click();
|
||||
|
||||
const createModalTree = page.getByRole('tree', {
|
||||
name: "Create Modal Tree"
|
||||
});
|
||||
const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
|
||||
const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
|
||||
name: parentFolder.name
|
||||
});
|
||||
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
|
||||
|
||||
const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
});
|
||||
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await childFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
|
||||
|
||||
const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
|
||||
name: grandchildFolder.name
|
||||
});
|
||||
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await grandchildFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('[aria-label="Cancel"]').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({
|
||||
// Move Child Folder from Parent Folder to My Items
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
}).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-link').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Link/
|
||||
}).click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
const myItemsPaneTreeItem = treePane.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
|
||||
// Expect that Child Folder is in My Items, the root folder
|
||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
|
||||
expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -26,6 +26,7 @@ This test suite is dedicated to tests which verify Open MCT's Notification funct
|
||||
|
||||
// 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', () => {
|
||||
@ -37,3 +38,42 @@ test.describe('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('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
|
||||
await page.locator('text=Renamed Condition Set').first().press('Enter');
|
||||
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');
|
||||
// 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,6 +24,7 @@ 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' });
|
||||
@ -31,8 +32,7 @@ test.describe('Display Layout', () => {
|
||||
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: "Test Sine Wave Generator"
|
||||
type: 'Sine Wave Generator'
|
||||
});
|
||||
});
|
||||
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
|
||||
@ -47,7 +47,14 @@ 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
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@ -74,7 +81,14 @@ 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
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@ -105,7 +119,14 @@ 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
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@ -115,7 +136,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 page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
|
||||
await sineWaveGeneratorTreeItem.nth(1).click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
@ -130,8 +151,7 @@ test.describe('Display Layout', () => {
|
||||
});
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: "Test Display Layout"
|
||||
type: 'Display Layout'
|
||||
});
|
||||
// Edit Display Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
@ -139,7 +159,14 @@ 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
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@ -152,7 +179,7 @@ test.describe('Display Layout', () => {
|
||||
await page.goto(sineWaveObject.url);
|
||||
|
||||
// Bring up context menu and remove
|
||||
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
|
||||
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
|
@ -25,26 +25,33 @@ 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',
|
||||
name: "Test Sine Wave Generator"
|
||||
type: 'Sine Wave Generator'
|
||||
});
|
||||
|
||||
// Create Clock Object
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: "Test Clock"
|
||||
clockObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock'
|
||||
});
|
||||
});
|
||||
test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ page }) => {
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
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',
|
||||
name: "Test Flexible Layout"
|
||||
type: 'Flexible Layout'
|
||||
});
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
@ -52,8 +59,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 page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
|
||||
await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty');
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await clockTreeItem.dragTo(page.locator('.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');
|
||||
@ -65,10 +72,15 @@ 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.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout',
|
||||
name: "Test Flexible Layout"
|
||||
type: 'Flexible Layout'
|
||||
});
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
@ -76,7 +88,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 page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@ -86,7 +98,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 page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
|
||||
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
@ -98,10 +110,16 @@ test.describe('Flexible Layout', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/3117'
|
||||
});
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
|
||||
// Create a Flexible Layout
|
||||
const flexibleLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout',
|
||||
name: "Test Flexible Layout"
|
||||
type: 'Flexible Layout'
|
||||
});
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
@ -109,7 +127,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 page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@ -122,7 +140,7 @@ test.describe('Flexible Layout', () => {
|
||||
await page.goto(sineWaveObject.url);
|
||||
|
||||
// Bring up context menu and remove
|
||||
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
|
||||
await sineWaveGeneratorTreeItem.first().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,6 +207,58 @@ 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',
|
||||
@ -345,13 +397,11 @@ test.describe('Example Imagery in Time Strip', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
timeStripObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time Strip',
|
||||
name: 'Time Strip'.concat(' ', uuid())
|
||||
type: 'Time Strip'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Imagery',
|
||||
name: 'Example Imagery'.concat(' ', uuid()),
|
||||
parent: timeStripObject.uuid
|
||||
});
|
||||
// Navigate to timestrip
|
||||
@ -362,17 +412,28 @@ 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 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();
|
||||
|
||||
// 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
|
||||
await page.locator('.c-imagery-tsv-container').click();
|
||||
// get image of view large container
|
||||
|
||||
// Get the img src of the large view image
|
||||
const viewLargeImg = page.locator('img.c-imagery__main-image__image');
|
||||
const viewLargeImgSrc = await viewLargeImg.getAttribute('src');
|
||||
expect(viewLargeImgSrc).toBeTruthy();
|
||||
expect(viewLargeImgSrc).toEqual(hoveredImgSrc);
|
||||
|
||||
// Verify that the image in the large view is the same as the hovered thumbnail
|
||||
expect(viewLargeImgSrc).toEqual(hoveredThumbnailImgSrc.split('?')[0]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -389,6 +450,12 @@ 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,8 +24,6 @@
|
||||
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');
|
||||
@ -265,71 +263,77 @@ 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' });
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
// Create Notebook
|
||||
const notebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: "Entry Link Test"
|
||||
});
|
||||
|
||||
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?
|
||||
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.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('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('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
|
||||
//Add snapshot to container
|
||||
//Verify PNG, JPG, and Annotate buttons work correctly
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,134 @@
|
||||
/*****************************************************************************
|
||||
* 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,6 +76,7 @@ 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);
|
||||
|
||||
@ -148,14 +149,17 @@ 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-popup-menu-button').click(); // embed popup menu
|
||||
await page.locator('.c-ne__embed__name .c-icon-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-popup-menu-button').click(); // embed popup menu
|
||||
await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
|
||||
|
||||
const embedMenu = page.locator('body >> .c-menu');
|
||||
await expect(embedMenu).not.toContainText('Remove This Embed');
|
||||
|
@ -57,12 +57,14 @@ 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.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
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();
|
||||
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
@ -71,8 +73,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") >> nth = ${iteration}`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Select the "Science" tag
|
||||
@ -84,8 +86,10 @@ 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();
|
||||
@ -126,13 +130,12 @@ 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="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Tags Inspector"]')).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");
|
||||
@ -194,11 +197,18 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
page.goto('./#/browse/mine?hideTree=false'),
|
||||
page.click('.c-disclosure-triangle')
|
||||
]);
|
||||
// Click Clock
|
||||
await page.click(`text=${clock.name}`);
|
||||
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
// Click Clock
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: clock.name
|
||||
}).click();
|
||||
// Click Notebook
|
||||
await page.click(`text=${notebook.name}`);
|
||||
await page.getByRole('treeitem', {
|
||||
name: notebook.name
|
||||
}).click();
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
|
@ -0,0 +1,156 @@
|
||||
/*****************************************************************************
|
||||
* 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.describe('ExportAsJSON', () => {
|
||||
test.fixme('ExportAsJSON', () => {
|
||||
test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
@ -156,7 +156,7 @@ async function turnOffAutoscale(page) {
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
|
||||
// uncheck autoscale
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck();
|
||||
await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck();
|
||||
|
||||
// save
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
|
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 18 KiB |
@ -205,7 +205,8 @@ async function enableEditMode(page) {
|
||||
*/
|
||||
async function enableLogMode(page) {
|
||||
// turn on log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
|
||||
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').check();
|
||||
// await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -213,7 +214,7 @@ async function enableLogMode(page) {
|
||||
*/
|
||||
async function disableLogMode(page) {
|
||||
// turn off log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
|
||||
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
|
124
e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js
Normal file
@ -0,0 +1,124 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
|
||||
necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Overlay Plot', () => {
|
||||
test('Plot legend color is in sync with plot series color', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Overlay Plot"
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
// navigate to plot series color palette
|
||||
await page.click('.l-browse-bar__actions__edit');
|
||||
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
|
||||
await page.locator('.c-click-swatch--menu').click();
|
||||
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();
|
||||
|
||||
// gets color for swatch located in legend
|
||||
const element = await page.waitForSelector('.plot-series-color-swatch');
|
||||
const color = await element.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-color');
|
||||
});
|
||||
|
||||
expect(color).toBe('rgb(255, 166, 61)');
|
||||
});
|
||||
test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Overlay Plot"
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg a',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg b',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg c',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg d',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg e',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
// Expand the elements pool vertically
|
||||
await page.locator('.l-pane__handle').nth(2).hover({ trial: true });
|
||||
await page.mouse.down();
|
||||
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();
|
||||
});
|
||||
});
|
85
e2e/tests/functional/recentObjects.e2e.spec.js
Normal file
@ -0,0 +1,85 @@
|
||||
/*****************************************************************************
|
||||
* 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('text=Clock A').click()
|
||||
page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click()
|
||||
]);
|
||||
await expect(page.locator('.is-object-type-clock')).toBeVisible();
|
||||
|
||||
|
@ -116,7 +116,9 @@ async function getAndAssertTreeItems(page, expected) {
|
||||
* @param {string} name
|
||||
*/
|
||||
async function expandTreePaneItemByName(page, name) {
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
|
||||
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
|
||||
await expandTriangle.click();
|
||||
|
@ -57,7 +57,7 @@ test.describe('Visual - Tree Pane', () => {
|
||||
name: 'Z Clock'
|
||||
});
|
||||
|
||||
const treePane = "#tree-pane";
|
||||
const treePane = "[role=tree][aria-label='Main Tree']";
|
||||
|
||||
await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, {
|
||||
scope: treePane
|
||||
@ -94,7 +94,7 @@ test.describe('Visual - Tree Pane', () => {
|
||||
* @param {string} name
|
||||
*/
|
||||
async function expandTreePaneItemByName(page, name) {
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const treePane = page.getByTestId('tree-pane');
|
||||
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
|
||||
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
|
||||
await expandTriangle.click();
|
||||
|
@ -65,7 +65,7 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
this.user = undefined;
|
||||
this.loggedIn = false;
|
||||
this.autoLoginUser = undefined;
|
||||
this.status = STATUSES[1];
|
||||
this.status = STATUSES[0];
|
||||
this.pollQuestion = undefined;
|
||||
this.defaultStatusRole = defaultStatusRole;
|
||||
|
||||
@ -124,6 +124,7 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
}
|
||||
|
||||
setStatusForRole(role, status) {
|
||||
status.timestamp = Date.now();
|
||||
this.status = status;
|
||||
this.emit('statusChange', {
|
||||
role,
|
||||
@ -133,14 +134,23 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
return true;
|
||||
}
|
||||
|
||||
getPollQuestion() {
|
||||
return Promise.resolve({
|
||||
question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
// eslint-disable-next-line require-await
|
||||
async getPollQuestion() {
|
||||
if (this.pollQuestion) {
|
||||
return this.pollQuestion;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
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,8 +37,9 @@ define([
|
||||
infinityValues: false
|
||||
};
|
||||
|
||||
function GeneratorProvider(openmct) {
|
||||
this.workerInterface = new WorkerInterface(openmct);
|
||||
function GeneratorProvider(openmct, StalenessProvider) {
|
||||
this.openmct = openmct;
|
||||
this.workerInterface = new WorkerInterface(openmct, StalenessProvider);
|
||||
}
|
||||
|
||||
GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
|
||||
@ -81,6 +82,7 @@ define([
|
||||
workerRequest[prop] = Number(workerRequest[prop]);
|
||||
});
|
||||
|
||||
workerRequest.id = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
workerRequest.name = domainObject.name;
|
||||
|
||||
return workerRequest;
|
||||
|
151
example/generator/SinewaveStalenessProvider.js
Normal file
@ -0,0 +1,151 @@
|
||||
/*****************************************************************************
|
||||
* 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,14 +25,24 @@ define([
|
||||
], function (
|
||||
{ v4: uuid }
|
||||
) {
|
||||
function WorkerInterface(openmct) {
|
||||
function WorkerInterface(openmct, StalenessProvider) {
|
||||
// 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];
|
||||
@ -83,11 +93,12 @@ define([
|
||||
};
|
||||
|
||||
WorkerInterface.prototype.subscribe = function (request, cb) {
|
||||
function callback(message) {
|
||||
cb(message.data);
|
||||
}
|
||||
|
||||
var messageId = this.dispatch('subscribe', request, callback);
|
||||
const id = request.id;
|
||||
const messageId = this.dispatch('subscribe', request, (message) => {
|
||||
if (!this.staleTelemetryIds[id]) {
|
||||
cb(message.data);
|
||||
}
|
||||
});
|
||||
|
||||
return function () {
|
||||
this.dispatch('unsubscribe', {
|
||||
|
@ -20,158 +20,163 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
"./GeneratorProvider",
|
||||
"./SinewaveLimitProvider",
|
||||
"./StateGeneratorProvider",
|
||||
"./GeneratorMetadataProvider"
|
||||
], function (
|
||||
GeneratorProvider,
|
||||
SinewaveLimitProvider,
|
||||
StateGeneratorProvider,
|
||||
GeneratorMetadataProvider
|
||||
) {
|
||||
import GeneratorProvider from "./GeneratorProvider";
|
||||
import SinewaveLimitProvider from "./SinewaveLimitProvider";
|
||||
import SinewaveStalenessProvider from "./SinewaveStalenessProvider";
|
||||
import StateGeneratorProvider from "./StateGeneratorProvider";
|
||||
import GeneratorMetadataProvider from "./GeneratorMetadataProvider";
|
||||
|
||||
return function (openmct) {
|
||||
export default 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"
|
||||
]
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
duration: 5
|
||||
};
|
||||
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
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
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"
|
||||
]
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
period: 10,
|
||||
amplitude: 1,
|
||||
offset: 0,
|
||||
dataRateInHz: 1,
|
||||
phase: 0,
|
||||
randomness: 0,
|
||||
loadDelay: 0,
|
||||
infinityValues: false
|
||||
};
|
||||
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"
|
||||
]
|
||||
}
|
||||
});
|
||||
],
|
||||
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));
|
||||
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
|
||||
openmct.telemetry.addProvider(new SinewaveLimitProvider());
|
||||
};
|
||||
|
||||
});
|
||||
openmct.telemetry.addProvider(new GeneratorProvider(openmct, stalenessProvider));
|
||||
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
|
||||
openmct.telemetry.addProvider(new SinewaveLimitProvider());
|
||||
openmct.telemetry.addProvider(stalenessProvider);
|
||||
}
|
||||
|
@ -107,6 +107,15 @@ export default function () {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Image Thumbnail',
|
||||
key: 'thumbnail-url',
|
||||
format: 'thumbnail',
|
||||
hints: {
|
||||
thumbnail: 1
|
||||
},
|
||||
source: 'url'
|
||||
},
|
||||
{
|
||||
name: 'Image Download Name',
|
||||
key: 'imageDownloadName',
|
||||
@ -143,6 +152,16 @@ 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());
|
||||
@ -242,6 +261,13 @@ 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,
|
||||
@ -251,6 +277,7 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
|
||||
sunOrientation: getCompassValues(0, 360),
|
||||
cameraPan: getCompassValues(0, 360),
|
||||
heading: getCompassValues(0, 360),
|
||||
transformations: navCamTransformations,
|
||||
imageDownloadName
|
||||
};
|
||||
}
|
||||
|
@ -22,12 +22,13 @@
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.32.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.11.2",
|
||||
"eslint-plugin-playwright": "0.12.0",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"file-saver": "2.0.5",
|
||||
"git-rev-sync": "3.0.2",
|
||||
"git-revision-webpack-plugin": "5.0.0",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "4.0.1",
|
||||
"jasmine-core": "4.5.0",
|
||||
@ -41,6 +42,7 @@
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"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",
|
||||
@ -54,6 +56,7 @@
|
||||
"plotly.js-gl2d-dist": "2.17.1",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.8.1",
|
||||
"sass": "1.57.1",
|
||||
"sass-loader": "13.2.0",
|
||||
"sinon": "15.0.1",
|
||||
@ -98,7 +101,8 @@
|
||||
"cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
|
||||
"cov:e2e:stable:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
|
||||
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
|
||||
"prepare": "npm run build:prod && npx tsc"
|
||||
"prepare": "npm run build:prod && npx tsc",
|
||||
"prepack": "git rev-parse HEAD >> version.info; git rev-parse --abbrev-ref HEAD >> version.info"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -256,6 +256,15 @@ 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,6 +52,29 @@ 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 {
|
||||
|
||||
/**
|
||||
@ -81,24 +104,26 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the a generic annotation
|
||||
* Creates an annotation on a given domain object (e.g., a plot) and a set of targets (e.g., telemetry objects)
|
||||
* @typedef {Object} CreateAnnotationOptions
|
||||
* @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
|
||||
* @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)
|
||||
*/
|
||||
/**
|
||||
* @method create
|
||||
* @param {CreateAnnotationOptions} options
|
||||
* @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object
|
||||
* @returns {Promise<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}) {
|
||||
async create({name, domainObject, annotationType, tags, contentText, targets, targetDomainObjects}) {
|
||||
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
|
||||
throw new Error(`Unknown annotation type: ${annotationType}`);
|
||||
}
|
||||
@ -107,6 +132,10 @@ 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);
|
||||
@ -139,7 +168,9 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const success = await this.openmct.objects.save(createdObject);
|
||||
if (success) {
|
||||
this.emit('annotationCreated', createdObject);
|
||||
this.#updateAnnotationModified(domainObject);
|
||||
Object.values(targetDomainObjects).forEach(targetDomainObject => {
|
||||
this.#updateAnnotationModified(targetDomainObject);
|
||||
});
|
||||
|
||||
return createdObject;
|
||||
} else {
|
||||
@ -147,8 +178,15 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
#updateAnnotationModified(domainObject) {
|
||||
this.openmct.objects.mutate(domainObject, this.ANNOTATION_LAST_CREATED, Date.now());
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -162,7 +200,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @method isAnnotation
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} domainObject domainObject the domainObject in question
|
||||
* @param {DomainObject} domainObject the domainObject in question
|
||||
* @returns {Boolean} Returns true if the domain object is an annotation
|
||||
*/
|
||||
isAnnotation(domainObject) {
|
||||
@ -190,56 +228,19 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @method getAnnotations
|
||||
* @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
|
||||
* @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
|
||||
*/
|
||||
async getAnnotations(query) {
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
|
||||
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();
|
||||
|
||||
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 {import('../objects/ObjectAPI').DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
|
||||
* @param {DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
|
||||
*/
|
||||
deleteAnnotations(annotations) {
|
||||
if (!annotations) {
|
||||
@ -255,7 +256,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @method deleteAnnotations
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} existingAnnotation - An annotations to undelete (set _deleted to false)
|
||||
* @param {DomainObject} annotation - An annotation to undelete (set _deleted to false)
|
||||
*/
|
||||
unDeleteAnnotation(annotation) {
|
||||
if (!annotation) {
|
||||
@ -265,6 +266,39 @@ 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 [];
|
||||
@ -283,12 +317,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
#addTagMetaInformationToResults(results, matchingTagKeys) {
|
||||
const tagsAddedToResults = results.map(result => {
|
||||
const fullTagModels = result.tags.map(tagKey => {
|
||||
const tagModel = this.availableTags[tagKey];
|
||||
tagModel.tagID = tagKey;
|
||||
|
||||
return tagModel;
|
||||
});
|
||||
const fullTagModels = this.#addTagMetaInformationToTags(result.tags);
|
||||
|
||||
return {
|
||||
fullTagModels,
|
||||
@ -338,6 +367,33 @@ 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"
|
||||
@ -360,7 +416,8 @@ 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 resultsWithValidPath;
|
||||
return breakApartSeparateTargets;
|
||||
}
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ 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);
|
||||
@ -124,27 +125,39 @@ 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.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
});
|
||||
it("can delete a tag", async () => {
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
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.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(() => {
|
||||
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
|
||||
}).toThrow();
|
||||
});
|
||||
it("can remove all tags", async () => {
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(() => {
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
@ -152,13 +165,13 @@ describe("The Annotation API", () => {
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
});
|
||||
it("can add/delete/add a tag", async () => {
|
||||
let annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
let annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
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.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
|
@ -30,7 +30,7 @@
|
||||
id="fileElem"
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".json"
|
||||
:accept="acceptableFileTypes"
|
||||
style="display:none"
|
||||
>
|
||||
<button
|
||||
@ -72,6 +72,13 @@ 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() {
|
||||
@ -80,7 +87,13 @@ export default {
|
||||
methods: {
|
||||
handleFiles() {
|
||||
const fileList = this.$refs.fileInput.files;
|
||||
this.readFile(fileList[0]);
|
||||
const file = fileList[0];
|
||||
|
||||
if (this.acceptableFileTypes === 'application/json') {
|
||||
this.readFile(file);
|
||||
} else {
|
||||
this.handleRawFile(file);
|
||||
}
|
||||
},
|
||||
readFile(file) {
|
||||
const self = this;
|
||||
@ -104,6 +117,21 @@ 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();
|
||||
},
|
||||
|
@ -189,13 +189,11 @@ 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 {AbortController.signal} abortSignal (optional) signal to abort fetch requests
|
||||
* @param {boolean} forceRemote defaults to false. If true, will skip cached and
|
||||
* @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} a promise which will resolve when the domain object
|
||||
* @returns {Promise<DomainObject>} 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) {
|
||||
@ -220,7 +218,7 @@ export default class ObjectAPI {
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('No Provider Matched');
|
||||
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}}"`);
|
||||
}
|
||||
|
||||
if (!provider.get) {
|
||||
@ -740,6 +738,46 @@ 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
|
||||
|
@ -36,6 +36,7 @@ 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;
|
||||
@ -114,6 +115,10 @@ export default class TelemetryAPI {
|
||||
if (provider.supportsLimits) {
|
||||
this.limitProviders.unshift(provider);
|
||||
}
|
||||
|
||||
if (provider.supportsStaleness) {
|
||||
this.stalenessProviders.unshift(provider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -125,7 +130,7 @@ export default class TelemetryAPI {
|
||||
return provider.supportsSubscribe.apply(provider, args);
|
||||
}
|
||||
|
||||
return this.subscriptionProviders.filter(supportsDomainObject)[0];
|
||||
return this.subscriptionProviders.find(supportsDomainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,25 +143,25 @@ export default class TelemetryAPI {
|
||||
return provider.supportsRequest.apply(provider, args);
|
||||
}
|
||||
|
||||
return this.requestProviders.filter(supportsDomainObject)[0];
|
||||
return this.requestProviders.find(supportsDomainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findMetadataProvider(domainObject) {
|
||||
return this.metadataProviders.filter(function (p) {
|
||||
return p.supportsMetadata(domainObject);
|
||||
})[0];
|
||||
return this.metadataProviders.find((provider) => {
|
||||
return provider.supportsMetadata(domainObject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findLimitEvaluator(domainObject) {
|
||||
return this.limitProviders.filter(function (p) {
|
||||
return p.supportsLimits(domainObject);
|
||||
})[0];
|
||||
return this.limitProviders.find((provider) => {
|
||||
return provider.supportsLimits(domainObject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -351,6 +356,101 @@ 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
|
||||
@ -661,6 +761,29 @@ 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,5 +291,6 @@ export default class StatusAPI extends EventEmitter {
|
||||
* The Status type
|
||||
* @typedef {Object} Status
|
||||
* @property {String} key - A unique identifier for this status
|
||||
* @property {Number} label - A human readable label for this status
|
||||
* @property {String} label - A human readable label for this status
|
||||
* @property {Number} timestamp - The time that the status was set.
|
||||
*/
|
||||
|
@ -29,7 +29,7 @@
|
||||
<td class="js-second-data">{{ formattedTimestamp }}</td>
|
||||
<td
|
||||
class="js-third-data"
|
||||
:class="valueClass"
|
||||
:class="valueClasses"
|
||||
>{{ value }}</td>
|
||||
<td
|
||||
v-if="hasUnits"
|
||||
@ -63,6 +63,12 @@ export default {
|
||||
hasUnits: {
|
||||
type: Boolean,
|
||||
requred: true
|
||||
},
|
||||
isStale: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -81,14 +87,22 @@ export default {
|
||||
|
||||
return this.formats[this.valueKey].format(this.datum);
|
||||
},
|
||||
valueClass() {
|
||||
if (!this.datum) {
|
||||
return '';
|
||||
valueClasses() {
|
||||
let classes = [];
|
||||
|
||||
if (this.isStale) {
|
||||
classes.push('is-stale');
|
||||
}
|
||||
|
||||
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
|
||||
if (this.datum) {
|
||||
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
|
||||
|
||||
return limit ? limit.cssClass : '';
|
||||
if (limit) {
|
||||
classes.push(limit.cssClass);
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
|
||||
},
|
||||
formattedTimestamp() {
|
||||
|
@ -21,7 +21,10 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver">
|
||||
<div
|
||||
class="c-lad-table-wrapper u-style-receiver js-style-receiver"
|
||||
:class="staleClass"
|
||||
>
|
||||
<table class="c-table c-lad-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -38,6 +41,7 @@
|
||||
:domain-object="ladRow.domainObject"
|
||||
:path-to-table="objectPath"
|
||||
:has-units="hasUnits"
|
||||
:is-stale="staleObjects.includes(ladRow.key)"
|
||||
@rowContextClick="updateViewContext"
|
||||
/>
|
||||
</tbody>
|
||||
@ -46,7 +50,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import LadRow from './LADRow.vue';
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -66,7 +72,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
viewContext: {}
|
||||
viewContext: {},
|
||||
staleObjects: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -80,6 +87,13 @@ export default {
|
||||
});
|
||||
|
||||
return itemsWithUnits.length !== 0;
|
||||
},
|
||||
staleClass() {
|
||||
if (this.staleObjects.length !== 0) {
|
||||
return 'is-stale';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -88,11 +102,17 @@ 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) {
|
||||
@ -101,23 +121,55 @@ 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) {
|
||||
let index = this.items.findIndex(item => this.openmct.objects.makeKeyString(identifier) === item.key);
|
||||
const SKIP_CHECK = true;
|
||||
const keystring = this.openmct.objects.makeKeyString(identifier);
|
||||
const index = this.items.findIndex(item => keystring === item.key);
|
||||
|
||||
this.items.splice(index, 1);
|
||||
|
||||
this.stalenessSubscription[keystring].unsubscribe();
|
||||
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
|
||||
},
|
||||
reorder(reorderPlan) {
|
||||
let oldItems = this.items.slice();
|
||||
const oldItems = this.items.slice();
|
||||
reorderPlan.forEach((reorderEvent) => {
|
||||
this.$set(this.items, reorderEvent.newIndex, oldItems[reorderEvent.oldIndex]);
|
||||
});
|
||||
},
|
||||
metadataHasUnits(valueMetadatas) {
|
||||
let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
|
||||
const 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,42 +21,50 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<td colspan="10">
|
||||
{{ ladTable.domainObject.name }}
|
||||
</td>
|
||||
<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>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template
|
||||
v-for="ladTable in ladTableObjects"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import LadRow from './LADRow.vue';
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -74,7 +82,8 @@ export default {
|
||||
ladTableObjects: [],
|
||||
ladTelemetryObjects: {},
|
||||
compositions: [],
|
||||
viewContext: {}
|
||||
viewContext: {},
|
||||
staleObjects: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -95,6 +104,13 @@ export default {
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
staleClass() {
|
||||
if (this.staleObjects.length !== 0) {
|
||||
return 'is-stale';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -103,6 +119,8 @@ 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);
|
||||
@ -112,6 +130,11 @@ 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) {
|
||||
@ -160,18 +183,57 @@ 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 => this.openmct.objects.makeKeyString(identifier) === telemetryObject.key);
|
||||
let index = telemetryObjects.findIndex(telemetryObject => keystring === 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,7 +160,8 @@ export default class Condition extends EventEmitter {
|
||||
}
|
||||
|
||||
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
|
||||
criterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
criterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
if (!this.criteria) {
|
||||
this.criteria = [];
|
||||
}
|
||||
@ -191,12 +192,14 @@ 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('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
|
||||
newCriterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
newCriterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
|
||||
let criterion = found.item;
|
||||
criterion.unsubscribe();
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
|
||||
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
newCriterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
this.criteria.splice(found.index, 1, newCriterion);
|
||||
}
|
||||
}
|
||||
@ -205,12 +208,9 @@ 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('telemetryIsStale', (obj) => {
|
||||
this.handleStaleCriterion(obj);
|
||||
});
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
criterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
criterion.destroy();
|
||||
this.criteria.splice(found.index, 1);
|
||||
|
||||
@ -227,7 +227,7 @@ export default class Condition extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
handleStaleCriterion(updatedCriterion) {
|
||||
handleOldTelemetryCriterion(updatedCriterion) {
|
||||
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
|
||||
let latestTimestamp = {};
|
||||
latestTimestamp = getLatestTimestamp(
|
||||
@ -239,6 +239,11 @@ 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,8 +82,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Condition from './Condition.vue';
|
||||
import ConditionManager from '../ConditionManager';
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -139,6 +141,13 @@ 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);
|
||||
@ -150,6 +159,7 @@ export default {
|
||||
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
|
||||
this.conditionManager.on('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
|
||||
this.updateDefaultCondition();
|
||||
this.stalenessSubscription = {};
|
||||
},
|
||||
methods: {
|
||||
handleConditionSetResultUpdated(data) {
|
||||
@ -210,19 +220,57 @@ 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) {
|
||||
let index = this.telemetryObjs.findIndex(obj => {
|
||||
const keyString = this.openmct.objects.makeKeyString(identifier);
|
||||
const index = this.telemetryObjs.findIndex(obj => {
|
||||
let objId = this.openmct.objects.makeKeyString(obj.identifier);
|
||||
let id = this.openmct.objects.makeKeyString(identifier);
|
||||
|
||||
return objId === id;
|
||||
return objId === keyString;
|
||||
});
|
||||
|
||||
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,7 +21,10 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-cs">
|
||||
<div
|
||||
class="c-cs"
|
||||
:class="{'is-stale': isStale }"
|
||||
>
|
||||
<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>
|
||||
@ -50,6 +53,7 @@
|
||||
@conditionSetResultUpdated="updateCurrentOutput"
|
||||
@updateDefaultOutput="updateDefaultOutput"
|
||||
@telemetryUpdated="updateTelemetry"
|
||||
@telemetryStaleness="handleStaleness"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -73,9 +77,15 @@ export default {
|
||||
currentConditionOutput: '',
|
||||
defaultConditionOutput: '',
|
||||
telemetryObjs: [],
|
||||
testData: {}
|
||||
testData: {},
|
||||
staleObjects: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isStale() {
|
||||
return this.staleObjects.length !== 0;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.conditionSetIdentifier = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.testData = {
|
||||
@ -95,6 +105,18 @@ 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'">seconds</span>
|
||||
<span v-if="criterion.metadata === 'dataReceived' && criterion.operation.name === IS_OLD_KEY">seconds</span>
|
||||
</template>
|
||||
<span v-else>
|
||||
<span
|
||||
@ -122,7 +122,7 @@
|
||||
|
||||
<script>
|
||||
import { OPERATIONS, INPUT_TYPES } from '../utils/operations';
|
||||
import {TRIGGER_CONJUNCTION} from "../utils/constants";
|
||||
import { TRIGGER_CONJUNCTION, IS_OLD_KEY, IS_STALE_KEY } from "../utils/constants";
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
@ -153,7 +153,8 @@ export default {
|
||||
rowLabel: '',
|
||||
operationFormat: '',
|
||||
enumerations: [],
|
||||
inputTypes: INPUT_TYPES
|
||||
inputTypes: INPUT_TYPES,
|
||||
IS_OLD_KEY
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -164,7 +165,7 @@ export default {
|
||||
},
|
||||
filteredOps: function () {
|
||||
if (this.criterion.metadata === 'dataReceived') {
|
||||
return this.operations.filter(op => op.name === 'isStale');
|
||||
return this.operations.filter(op => op.name === IS_OLD_KEY || op.name === IS_STALE_KEY);
|
||||
} else {
|
||||
return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1);
|
||||
}
|
||||
|
@ -54,6 +54,10 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-stale {
|
||||
@include isStaleHolder();
|
||||
}
|
||||
|
||||
/************************** CONDITION SET LAYOUT */
|
||||
&__current-output {
|
||||
flex: 0 0 auto;
|
||||
|
@ -21,8 +21,9 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import TelemetryCriterion from './TelemetryCriterion';
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
import { evaluateResults } from "../utils/evaluator";
|
||||
import {getLatestTimestamp, subscribeForStaleness} from '../utils/time';
|
||||
import { getLatestTimestamp, checkIfOld } from '../utils/time';
|
||||
import { getOperatorText } from "@/plugins/condition/utils/operations";
|
||||
|
||||
export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
@ -38,13 +39,41 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
initialize() {
|
||||
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
|
||||
this.telemetryDataCache = {};
|
||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||
this.subscribeForStaleData(this.telemetryObjects || {});
|
||||
|
||||
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
|
||||
this.checkForOldData(this.telemetryObjects || {});
|
||||
}
|
||||
|
||||
if (this.isValid() && this.isStalenessCheck()) {
|
||||
this.subscribeToStaleness(this.telemetryObjects || {});
|
||||
}
|
||||
}
|
||||
|
||||
subscribeForStaleData(telemetryObjects) {
|
||||
checkForOldData(telemetryObjects) {
|
||||
if (!this.ageCheck) {
|
||||
this.ageCheck = {};
|
||||
}
|
||||
|
||||
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 = {};
|
||||
}
|
||||
@ -52,20 +81,27 @@ 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] = subscribeForStaleness((data) => {
|
||||
this.handleStaleTelemetry(id, data);
|
||||
}, this.input[0] * 1000);
|
||||
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);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleStaleTelemetry(id, data) {
|
||||
handleStaleTelemetry(id, stalenessResponse) {
|
||||
if (this.telemetryDataCache) {
|
||||
this.telemetryDataCache[id] = true;
|
||||
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
|
||||
}
|
||||
if (this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
|
||||
this.telemetryDataCache[id] = stalenessResponse.isStale;
|
||||
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
|
||||
|
||||
this.emitEvent('telemetryIsStale', data);
|
||||
this.emitEvent('telemetryStaleness');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isValid() {
|
||||
@ -75,8 +111,13 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
updateTelemetryObjects(telemetryObjects) {
|
||||
this.telemetryObjects = { ...telemetryObjects };
|
||||
this.removeTelemetryDataCache();
|
||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||
this.subscribeForStaleData(this.telemetryObjects || {});
|
||||
|
||||
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
|
||||
this.checkForOldData(this.telemetryObjects || {});
|
||||
}
|
||||
|
||||
if (this.isValid() && this.isStalenessCheck()) {
|
||||
this.subscribeToStaleness(this.telemetryObjects || {});
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,6 +132,9 @@ 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]);
|
||||
});
|
||||
}
|
||||
@ -125,10 +169,10 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
updateResult(data, telemetryObjects) {
|
||||
const validatedData = this.isValid() ? data : {};
|
||||
|
||||
if (validatedData) {
|
||||
if (this.isStalenessCheck()) {
|
||||
if (this.stalenessSubscription && this.stalenessSubscription[validatedData.id]) {
|
||||
this.stalenessSubscription[validatedData.id].update(validatedData);
|
||||
if (validatedData && !this.isStalenessCheck()) {
|
||||
if (this.isOldCheck()) {
|
||||
if (this.ageCheck?.[validatedData.id]) {
|
||||
this.ageCheck[validatedData.id].update(validatedData);
|
||||
}
|
||||
|
||||
this.telemetryDataCache[validatedData.id] = false;
|
||||
@ -226,9 +270,17 @@ 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.clear);
|
||||
delete this.stalenessSubscription;
|
||||
Object.values(this.stalenessSubscription).forEach(subscription => {
|
||||
subscription.unsubscribe();
|
||||
subscription.stalenessUtils.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,10 @@
|
||||
*****************************************************************************/
|
||||
|
||||
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 { subscribeForStaleness } from "../utils/time";
|
||||
import { checkIfOld } from "../utils/time";
|
||||
|
||||
export default class TelemetryCriterion extends EventEmitter {
|
||||
|
||||
@ -44,7 +46,8 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
this.input = telemetryDomainObjectDefinition.input;
|
||||
this.metadata = telemetryDomainObjectDefinition.metadata;
|
||||
this.result = undefined;
|
||||
this.stalenessSubscription = undefined;
|
||||
this.ageCheck = undefined;
|
||||
this.unsubscribeFromStaleness = undefined;
|
||||
|
||||
this.initialize();
|
||||
this.emitEvent('criterionUpdated', this);
|
||||
@ -57,8 +60,13 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
}
|
||||
|
||||
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
|
||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||
this.subscribeForStaleData();
|
||||
|
||||
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
|
||||
this.checkForOldData();
|
||||
}
|
||||
|
||||
if (this.isValid() && this.isStalenessCheck()) {
|
||||
this.subscribeToStaleness();
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,25 +74,52 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
return this.telemetryObjectIdAsString && (this.telemetryObjectIdAsString === id);
|
||||
}
|
||||
|
||||
subscribeForStaleData() {
|
||||
if (this.stalenessSubscription) {
|
||||
this.stalenessSubscription.clear();
|
||||
checkForOldData() {
|
||||
if (this.ageCheck) {
|
||||
this.ageCheck.clear();
|
||||
}
|
||||
|
||||
this.stalenessSubscription = subscribeForStaleness(this.handleStaleTelemetry.bind(this), this.input[0] * 1000);
|
||||
this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000);
|
||||
}
|
||||
|
||||
handleStaleTelemetry(data) {
|
||||
handleOldTelemetry(data) {
|
||||
this.result = true;
|
||||
this.emitEvent('telemetryIsStale', data);
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_STALE_KEY;
|
||||
}
|
||||
|
||||
isValidInput() {
|
||||
@ -93,8 +128,13 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
|
||||
updateTelemetryObjects(telemetryObjects) {
|
||||
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
|
||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||
this.subscribeForStaleData();
|
||||
|
||||
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
|
||||
this.checkForOldData();
|
||||
}
|
||||
|
||||
if (this.isValid() && this.isStalenessCheck()) {
|
||||
this.subscribeToStaleness();
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,14 +170,17 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
|
||||
updateResult(data) {
|
||||
const validatedData = this.isValid() ? data : {};
|
||||
if (this.isStalenessCheck()) {
|
||||
if (this.stalenessSubscription) {
|
||||
this.stalenessSubscription.update(validatedData);
|
||||
}
|
||||
|
||||
this.result = false;
|
||||
} else {
|
||||
this.result = this.computeResult(validatedData);
|
||||
if (!this.isStalenessCheck()) {
|
||||
if (this.isOldCheck()) {
|
||||
if (this.ageCheck) {
|
||||
this.ageCheck.update(validatedData);
|
||||
}
|
||||
|
||||
this.result = false;
|
||||
} else {
|
||||
this.result = this.computeResult(validatedData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,8 +311,17 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
destroy() {
|
||||
delete this.telemetryObject;
|
||||
delete this.telemetryObjectIdAsString;
|
||||
if (this.stalenessSubscription) {
|
||||
delete this.stalenessSubscription;
|
||||
|
||||
if (this.ageCheck) {
|
||||
delete this.ageCheck;
|
||||
}
|
||||
|
||||
if (this.stalenessUtils) {
|
||||
this.stalenessUtils.destroy();
|
||||
}
|
||||
|
||||
if (this.unsubscribeFromStaleness) {
|
||||
this.unsubscribeFromStaleness();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ 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;
|
||||
@ -642,7 +643,7 @@ describe('the plugin', function () {
|
||||
|
||||
});
|
||||
|
||||
describe('the condition check for staleness', () => {
|
||||
describe('the condition check if old', () => {
|
||||
let conditionSetDomainObject;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -660,13 +661,13 @@ describe('the plugin', function () {
|
||||
"id": "39584410-cbf9-499e-96dc-76f27e69885d",
|
||||
"configuration": {
|
||||
"name": "Unnamed Condition",
|
||||
"output": "Any stale telemetry",
|
||||
"output": "Any old telemetry",
|
||||
"trigger": "all",
|
||||
"criteria": [
|
||||
{
|
||||
"id": "35400132-63b0-425c-ac30-8197df7d5862",
|
||||
"telemetry": "any",
|
||||
"operation": "isStale",
|
||||
"operation": IS_OLD_KEY,
|
||||
"input": [
|
||||
"0.2"
|
||||
],
|
||||
@ -674,7 +675,7 @@ describe('the plugin', function () {
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": "Match if all criteria are met: Any telemetry is stale after 5 seconds"
|
||||
"summary": "Match if all criteria are met: Any telemetry is old after 5 seconds"
|
||||
},
|
||||
{
|
||||
"isDefault": true,
|
||||
@ -708,7 +709,7 @@ describe('the plugin', function () {
|
||||
};
|
||||
});
|
||||
|
||||
it('should evaluate as stale when telemetry is not received in the allotted time', (done) => {
|
||||
it('should evaluate as old when telemetry is not received in the allotted time', (done) => {
|
||||
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
||||
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
||||
conditionMgr.telemetryObjects = {
|
||||
@ -717,7 +718,7 @@ describe('the plugin', function () {
|
||||
conditionMgr.updateConditionTelemetryObjects();
|
||||
setTimeout(() => {
|
||||
expect(mockListener).toHaveBeenCalledWith({
|
||||
output: 'Any stale telemetry',
|
||||
output: 'Any old telemetry',
|
||||
id: {
|
||||
namespace: '',
|
||||
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
|
||||
@ -729,7 +730,7 @@ describe('the plugin', function () {
|
||||
}, 400);
|
||||
});
|
||||
|
||||
it('should not evaluate as stale when telemetry is received in the allotted time', (done) => {
|
||||
it('should not evaluate as old 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,3 +59,6 @@ export const ERROR = {
|
||||
errorText: 'Condition not found'
|
||||
}
|
||||
};
|
||||
|
||||
export const IS_OLD_KEY = 'isStale';
|
||||
export const IS_STALE_KEY = 'isStale.new';
|
||||
|
@ -20,6 +20,8 @@
|
||||
* 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)));
|
||||
@ -295,7 +297,7 @@ export const OPERATIONS = [
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'isStale',
|
||||
name: IS_OLD_KEY,
|
||||
operation: function () {
|
||||
return false;
|
||||
},
|
||||
@ -305,6 +307,18 @@ 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';
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@ -316,5 +330,5 @@ export const INPUT_TYPES = {
|
||||
export function getOperatorText(operationName, values) {
|
||||
const found = OPERATIONS.find((operation) => operation.name === operationName);
|
||||
|
||||
return found ? found.getDescription(values) : '';
|
||||
return found?.getDescription(values) ?? '';
|
||||
}
|
||||
|
@ -51,26 +51,26 @@ export function getLatestTimestamp(
|
||||
return latest;
|
||||
}
|
||||
|
||||
export function subscribeForStaleness(callback, timeout) {
|
||||
let stalenessTimer = setTimeout(() => {
|
||||
clearTimeout(stalenessTimer);
|
||||
export function checkIfOld(callback, timeout) {
|
||||
let oldCheckTimer = setTimeout(() => {
|
||||
clearTimeout(oldCheckTimer);
|
||||
callback();
|
||||
}, timeout);
|
||||
|
||||
return {
|
||||
update: (data) => {
|
||||
if (stalenessTimer) {
|
||||
clearTimeout(stalenessTimer);
|
||||
if (oldCheckTimer) {
|
||||
clearTimeout(oldCheckTimer);
|
||||
}
|
||||
|
||||
stalenessTimer = setTimeout(() => {
|
||||
clearTimeout(stalenessTimer);
|
||||
oldCheckTimer = setTimeout(() => {
|
||||
clearTimeout(oldCheckTimer);
|
||||
callback(data);
|
||||
}, timeout);
|
||||
},
|
||||
clear: () => {
|
||||
if (stalenessTimer) {
|
||||
clearTimeout(stalenessTimer);
|
||||
if (oldCheckTimer) {
|
||||
clearTimeout(oldCheckTimer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { subscribeForStaleness } from "./time";
|
||||
import { checkIfOld } from "./time";
|
||||
|
||||
describe('time related utils', () => {
|
||||
let subscription;
|
||||
@ -27,11 +27,11 @@ describe('time related utils', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockListener = jasmine.createSpy('listener');
|
||||
subscription = subscribeForStaleness(mockListener, 100);
|
||||
subscription = checkIfOld(mockListener, 100);
|
||||
});
|
||||
|
||||
describe('subscribe for staleness', () => {
|
||||
it('should call listeners when stale', (done) => {
|
||||
describe('check if old', () => {
|
||||
it('should call listeners when old', (done) => {
|
||||
setTimeout(() => {
|
||||
expect(mockListener).toHaveBeenCalled();
|
||||
done();
|
||||
|
@ -31,7 +31,7 @@
|
||||
<div
|
||||
v-if="domainObject"
|
||||
class="c-telemetry-view u-style-receiver"
|
||||
:class="[statusClass]"
|
||||
:class="[itemClasses]"
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
@ -73,6 +73,7 @@
|
||||
<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];
|
||||
@ -102,7 +103,7 @@ export default {
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
mixins: [conditionalStylesMixin, stalenessMixin],
|
||||
inject: ['openmct', 'objectPath', 'currentView'],
|
||||
props: {
|
||||
item: {
|
||||
@ -137,8 +138,18 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
statusClass() {
|
||||
return (this.status) ? `is-status--${this.status}` : '';
|
||||
itemClasses() {
|
||||
let classes = [];
|
||||
|
||||
if (this.status) {
|
||||
classes.push(`is-status--${this.status}`);
|
||||
}
|
||||
|
||||
if (this.isStale) {
|
||||
classes.push('is-stale');
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
showLabel() {
|
||||
let displayMode = this.item.displayMode;
|
||||
@ -310,6 +321,7 @@ 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,6 +25,12 @@
|
||||
margin-right: $interiorMargin;
|
||||
}
|
||||
|
||||
&.is-stale {
|
||||
.c-telemetry-view__value {
|
||||
@include isStaleElement();
|
||||
}
|
||||
}
|
||||
|
||||
.c-frame & {
|
||||
@include abs();
|
||||
border: 1px solid transparent;
|
||||
|
@ -31,6 +31,7 @@ export default class DuplicateAction {
|
||||
this.priority = 7;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.transaction = null;
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
@ -45,7 +46,9 @@ export default class DuplicateAction {
|
||||
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier));
|
||||
}
|
||||
|
||||
onSave(changes) {
|
||||
async onSave(changes) {
|
||||
this.startTransaction();
|
||||
|
||||
let inNavigationPath = this.inNavigationPath();
|
||||
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
@ -59,7 +62,9 @@ export default class DuplicateAction {
|
||||
const parentDomainObjectpath = changes.location || [this.parent];
|
||||
const parent = parentDomainObjectpath[0];
|
||||
|
||||
return duplicationTask.duplicate(this.object, parent);
|
||||
await duplicationTask.duplicate(this.object, parent);
|
||||
|
||||
return this.saveTransaction();
|
||||
}
|
||||
|
||||
showForm(domainObject, parentDomainObject) {
|
||||
@ -142,4 +147,20 @@ 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="`c-gauge--${gaugeType}`"
|
||||
:class="gaugeClasses"
|
||||
:title="gaugeTitle"
|
||||
>
|
||||
<template v-if="typeDial">
|
||||
@ -347,12 +347,14 @@
|
||||
|
||||
<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;
|
||||
@ -403,6 +405,15 @@ 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;
|
||||
@ -553,6 +564,8 @@ export default {
|
||||
this.telemetryObject = domainObject;
|
||||
this.request();
|
||||
this.subscribe();
|
||||
|
||||
this.subscribeToStaleness(domainObject);
|
||||
},
|
||||
addedToComposition(domainObject) {
|
||||
if (this.telemetryObject) {
|
||||
@ -611,6 +624,8 @@ export default {
|
||||
this.unsubscribe = null;
|
||||
}
|
||||
|
||||
this.triggerUnsubscribeFromStaleness();
|
||||
|
||||
this.curVal = DEFAULT_CURRENT_VALUE;
|
||||
this.formats = null;
|
||||
this.limitHigh = '';
|
||||
|
@ -32,6 +32,15 @@ $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,6 +45,35 @@ 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,19 +26,18 @@
|
||||
:style="`width: 100%; height: 100%`"
|
||||
>
|
||||
<CompassHUD
|
||||
v-if="hasCameraFieldOfView"
|
||||
v-if="showCompassHUD"
|
||||
:sun-heading="sunHeading"
|
||||
:camera-angle-of-view="cameraAngleOfView"
|
||||
:camera-pan="cameraPan"
|
||||
/>
|
||||
<CompassRose
|
||||
v-if="hasCameraFieldOfView"
|
||||
:camera-angle-of-view="cameraAngleOfView"
|
||||
v-if="showCompassRose"
|
||||
:camera-pan="cameraPan"
|
||||
:compass-rose-sizing-classes="compassRoseSizingClasses"
|
||||
:heading="heading"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
:sun-heading="sunHeading"
|
||||
:transformations="transformations"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -47,18 +46,12 @@
|
||||
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
|
||||
@ -69,13 +62,19 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasCameraFieldOfView() {
|
||||
return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
|
||||
showCompassHUD() {
|
||||
return this.hasCameraPan && this.cameraAngleOfView > 0;
|
||||
},
|
||||
showCompassRose() {
|
||||
return (this.hasCameraPan || this.hasHeading) && 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;
|
||||
@ -84,8 +83,14 @@ export default {
|
||||
cameraPan() {
|
||||
return this.image.cameraPan;
|
||||
},
|
||||
hasCameraPan() {
|
||||
return this.cameraPan !== undefined;
|
||||
},
|
||||
cameraAngleOfView() {
|
||||
return CAMERA_ANGLE_OF_VIEW;
|
||||
return this.transformations?.cameraAngleOfView;
|
||||
},
|
||||
transformations() {
|
||||
return this.image.transformations;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -64,14 +64,14 @@
|
||||
class="c-cr__edge"
|
||||
width="100"
|
||||
height="100"
|
||||
fill="url(#paint0_radial)"
|
||||
fill="url(#gradient_edge)"
|
||||
/>
|
||||
<rect
|
||||
v-if="hasSunHeading"
|
||||
class="c-cr__sun"
|
||||
width="100"
|
||||
height="100"
|
||||
fill="url(#paint1_radial)"
|
||||
fill="url(#gradient_sun)"
|
||||
:style="sunHeadingStyle"
|
||||
/>
|
||||
|
||||
@ -107,9 +107,26 @@
|
||||
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="cameraPanStyle"
|
||||
:style="cameraHeadingStyle"
|
||||
>
|
||||
<g mask="url(#mask2)">
|
||||
<rect
|
||||
@ -128,19 +145,13 @@
|
||||
: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"
|
||||
@ -193,7 +204,7 @@
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial"
|
||||
id="gradient_edge"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
@ -201,7 +212,7 @@
|
||||
gradientTransform="translate(50 50) rotate(90) scale(50)"
|
||||
>
|
||||
<stop
|
||||
offset="0.751387"
|
||||
offset="0.6"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
<stop
|
||||
@ -210,7 +221,7 @@
|
||||
/>
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="paint1_radial"
|
||||
id="gradient_sun"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
@ -218,12 +229,17 @@
|
||||
gradientTransform="translate(50 -7) rotate(-90) scale(18.5)"
|
||||
>
|
||||
<stop
|
||||
offset="0.716377"
|
||||
offset="0.7"
|
||||
stop-color="#FFCC00"
|
||||
/>
|
||||
<stop
|
||||
offset="0.7"
|
||||
stop-color="#FFCC00"
|
||||
stop-opacity="0.6"
|
||||
/>
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#FF9900"
|
||||
stop-color="#FF6600"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</radialGradient>
|
||||
@ -238,10 +254,6 @@ import { throttle } from 'lodash';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
compassRoseSizingClasses: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
heading: {
|
||||
type: Number,
|
||||
required: true,
|
||||
@ -253,16 +265,13 @@ export default {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraAngleOfView: {
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
transformations: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
sizedImageDimensions: {
|
||||
type: Object,
|
||||
@ -275,11 +284,38 @@ 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.cameraPan) : 0;
|
||||
return this.lockCompass ? rotate(-this.cameraHeading) : 0;
|
||||
},
|
||||
cardinalTextRotateN() {
|
||||
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
|
||||
@ -297,6 +333,7 @@ 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 {
|
||||
@ -313,8 +350,8 @@ export default {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
};
|
||||
},
|
||||
cameraPanStyle() {
|
||||
const rotation = rotate(this.north, this.cameraPan);
|
||||
cameraHeadingStyle() {
|
||||
const rotation = rotate(this.north, this.cameraHeading);
|
||||
|
||||
return {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
@ -333,6 +370,24 @@ 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: #00B9C5;
|
||||
$interfaceKeyColor: #fff;
|
||||
$elemBg: rgba(black, 0.7);
|
||||
|
||||
@mixin sun($position: 'circle closest-side') {
|
||||
@ -100,13 +100,19 @@ $elemBg: rgba(black, 0.7);
|
||||
}
|
||||
|
||||
&__edge {
|
||||
opacity: 0.1;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&__sun {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&__cam {
|
||||
fill: $interfaceKeyColor;
|
||||
transform-origin: center;
|
||||
transform: scale(0.15);
|
||||
}
|
||||
|
||||
&__cam-fov-l,
|
||||
&__cam-fov-r {
|
||||
// Cam FOV indication
|
||||
@ -115,7 +121,6 @@ $elemBg: rgba(black, 0.7);
|
||||
}
|
||||
|
||||
&__nsew-text,
|
||||
&__spacecraft-body,
|
||||
&__ticks-major,
|
||||
&__ticks-minor {
|
||||
fill: $color;
|
||||
@ -166,3 +171,15 @@ $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.url"
|
||||
:src="`${image.thumbnailUrl || image.url}`"
|
||||
fetchpriority="low"
|
||||
@load="imageLoadCompleted"
|
||||
>
|
||||
|
@ -186,17 +186,17 @@ export default {
|
||||
item.remove();
|
||||
});
|
||||
let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
|
||||
imagery.forEach(item => {
|
||||
imagery.forEach(imageElm => {
|
||||
if (clearAllImagery) {
|
||||
item.remove();
|
||||
imageElm.remove();
|
||||
} else {
|
||||
const id = item.getAttributeNS(null, 'id');
|
||||
const id = imageElm.getAttributeNS(null, 'id');
|
||||
if (id) {
|
||||
const timestamp = id.replace(ID_PREFIX, '');
|
||||
if (!this.isImageryInBounds({
|
||||
time: timestamp
|
||||
})) {
|
||||
item.remove();
|
||||
imageElm.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -343,25 +343,25 @@ export default {
|
||||
imageElement.style.display = 'block';
|
||||
}
|
||||
},
|
||||
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
|
||||
updateExistingImageWrapper(existingImageWrapper, image, 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(item.time)}px`;
|
||||
existingImageWrapper.style.left = `${this.xScale(image.time)}px`;
|
||||
|
||||
let imageElement = existingImageWrapper.querySelector('img');
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
src: item.url
|
||||
src: image.thumbnailUrl || image.url
|
||||
});
|
||||
this.setImageDisplay(imageElement, showImagePlaceholders);
|
||||
},
|
||||
createImageWrapper(index, item, showImagePlaceholders) {
|
||||
const id = `${ID_PREFIX}${item.time}`;
|
||||
createImageWrapper(index, image, showImagePlaceholders) {
|
||||
const id = `${ID_PREFIX}${image.time}`;
|
||||
let imageWrapper = document.createElement('div');
|
||||
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
|
||||
imageWrapper.style.left = `${this.xScale(item.time)}px`;
|
||||
imageWrapper.style.left = `${this.xScale(image.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: item.url
|
||||
src: image.thumbnailUrl || image.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(item.time);
|
||||
this.expand(image.time);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -93,7 +93,6 @@
|
||||
></div>
|
||||
<Compass
|
||||
v-if="shouldDisplayCompass"
|
||||
:compass-rose-sizing-classes="compassRoseSizingClasses"
|
||||
:image="focusedImage"
|
||||
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
@ -172,7 +171,7 @@
|
||||
>
|
||||
<ImageThumbnail
|
||||
v-for="(image, index) in imageHistory"
|
||||
:key="image.url + image.time"
|
||||
:key="`${image.thumbnailUrl || image.url}${image.time}`"
|
||||
:image="image"
|
||||
:active="focusedImageIndex === index"
|
||||
:selected="focusedImageIndex === index && isPaused"
|
||||
@ -226,6 +225,9 @@ 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: {
|
||||
@ -298,18 +300,6 @@ 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
|
||||
@ -321,8 +311,8 @@ export default {
|
||||
},
|
||||
focusImageStyles() {
|
||||
return {
|
||||
'filter': `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
|
||||
'background-image':
|
||||
filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
|
||||
backgroundImage:
|
||||
`${this.imageUrl ? (
|
||||
`url(${this.imageUrl}),
|
||||
repeating-linear-gradient(
|
||||
@ -333,10 +323,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() {
|
||||
@ -347,11 +337,12 @@ export default {
|
||||
},
|
||||
imageWrapperStyle() {
|
||||
return {
|
||||
'cursor-zoom-in': this.cursorStates.showCursorZoomIn,
|
||||
'cursor-zoom-out': this.cursorStates.showCursorZoomOut,
|
||||
'pannable': this.cursorStates.isPannable,
|
||||
'paused unnsynced': this.isPaused && !this.isFixed,
|
||||
'stale': false
|
||||
cursorZoomIn: this.cursorStates.showCursorZoomIn,
|
||||
cursorZoomOut: this.cursorStates.showCursorZoomOut,
|
||||
pannable: this.cursorStates.isPannable,
|
||||
paused: this.isPaused && !this.isFixed,
|
||||
unsynced: this.isPaused && !this.isFixed,
|
||||
stale: false
|
||||
};
|
||||
},
|
||||
isImageNew() {
|
||||
@ -432,7 +423,6 @@ export default {
|
||||
shouldDisplayCompass() {
|
||||
const imageHeightAndWidth = this.sizedImageHeight !== 0
|
||||
&& this.sizedImageWidth !== 0;
|
||||
|
||||
const display = this.focusedImage !== undefined
|
||||
&& this.focusedImageNaturalAspectRatio !== undefined
|
||||
&& this.imageContainerWidth !== undefined
|
||||
@ -440,8 +430,9 @@ export default {
|
||||
&& imageHeightAndWidth
|
||||
&& this.zoomFactor === 1
|
||||
&& this.imagePanned !== true;
|
||||
const hasCameraConfigurations = this.focusedImage?.transformations !== undefined;
|
||||
|
||||
return display;
|
||||
return display && hasCameraConfigurations;
|
||||
},
|
||||
isSpacecraftPositionFresh() {
|
||||
let isFresh = undefined;
|
||||
@ -528,10 +519,10 @@ export default {
|
||||
const navigator = window.navigator.userAgent;
|
||||
|
||||
if (regexLinux.test(navigator)) {
|
||||
return 'Ctrl+Alt drag to pan';
|
||||
return LINUX_IMAGE_PAN_ALT_TEXT;
|
||||
}
|
||||
|
||||
return 'Alt drag to pan';
|
||||
return DEFAULT_IMAGE_PAN_ALT_TEXT;
|
||||
},
|
||||
viewableArea() {
|
||||
if (this.zoomFactor === 1) {
|
||||
@ -626,6 +617,7 @@ export default {
|
||||
this.spacecraftOrientationKeys = ['heading'];
|
||||
this.cameraKeys = ['cameraPan', 'cameraTilt'];
|
||||
this.sunKeys = ['sunOrientation'];
|
||||
this.transformationsKeys = ['transformations'];
|
||||
|
||||
// related telemetry
|
||||
await this.initializeRelatedTelemetry();
|
||||
@ -691,9 +683,9 @@ export default {
|
||||
},
|
||||
getVisibleLayerStyles(layer) {
|
||||
return {
|
||||
'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'}`
|
||||
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'}`
|
||||
};
|
||||
},
|
||||
setTimeContext() {
|
||||
@ -721,14 +713,20 @@ export default {
|
||||
&& visibleActions.find(action => action.key === 'large.view');
|
||||
|
||||
if (viewLargeAction && viewLargeAction.appliesTo(this.objectPath, this.currentView)) {
|
||||
viewLargeAction.onItemClicked();
|
||||
viewLargeAction.invoke(this.objectPath, this.currentView);
|
||||
}
|
||||
},
|
||||
async initializeRelatedTelemetry() {
|
||||
this.relatedTelemetry = new RelatedTelemetry(
|
||||
this.openmct,
|
||||
this.domainObject,
|
||||
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys]
|
||||
[
|
||||
...this.spacecraftPositionKeys,
|
||||
...this.spacecraftOrientationKeys,
|
||||
...this.cameraKeys,
|
||||
...this.sunKeys,
|
||||
...this.transformationsKeys
|
||||
]
|
||||
);
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
@ -765,26 +763,26 @@ export default {
|
||||
return mostRecent[valueKey];
|
||||
},
|
||||
loadVisibleLayers() {
|
||||
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;
|
||||
});
|
||||
}
|
||||
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;
|
||||
});
|
||||
}
|
||||
},
|
||||
persistVisibleLayers() {
|
||||
@ -837,6 +835,15 @@ 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 => {
|
||||
|
@ -29,7 +29,7 @@
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&.unnsynced{
|
||||
&.unsynced{
|
||||
@include sUnsynced();
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,9 @@
|
||||
*****************************************************************************/
|
||||
|
||||
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'],
|
||||
@ -32,13 +35,20 @@ export default {
|
||||
this.setDataTimeContext();
|
||||
this.openmct.objectViews.on('clearData', this.dataCleared);
|
||||
|
||||
// set
|
||||
// Get metadata and formatters
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
|
||||
|
||||
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.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
|
||||
this.imageDownloadNameMetadataValue = { ...this.metadata.valuesForHints([IMAGE_DOWNLOAD_NAME_HINT_KEY])[0]};
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
@ -105,12 +115,19 @@ export default {
|
||||
|
||||
return this.imageFormatter.format(datum);
|
||||
},
|
||||
formatImageThumbnailUrl(datum) {
|
||||
if (!datum || !this.imageThumbnailFormatter) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.imageThumbnailFormatter.format(datum);
|
||||
},
|
||||
formatTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dateTimeStr = this.timeFormatter.format(datum);
|
||||
const dateTimeStr = this.timeFormatter.format(datum);
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr.replace("T", " ");
|
||||
@ -118,7 +135,7 @@ export default {
|
||||
getImageDownloadName(datum) {
|
||||
let imageDownloadName = '';
|
||||
if (datum) {
|
||||
const key = this.imageDownloadNameHints.key;
|
||||
const key = this.imageDownloadNameMetadataValue.key;
|
||||
imageDownloadName = datum[key];
|
||||
}
|
||||
|
||||
@ -150,6 +167,7 @@ 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);
|
||||
|
||||
@ -157,13 +175,14 @@ export default {
|
||||
...datum,
|
||||
formattedTime,
|
||||
url,
|
||||
thumbnailUrl,
|
||||
time,
|
||||
imageDownloadName
|
||||
};
|
||||
},
|
||||
getFormatter(key) {
|
||||
let metadataValue = this.metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
const metadataValue = this.metadata.value(key) || { format: key };
|
||||
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
}
|
||||
|
@ -35,6 +35,10 @@ 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;
|
||||
@ -124,6 +128,16 @@ 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",
|
||||
@ -200,6 +214,11 @@ describe("The Imagery View Layouts", () => {
|
||||
|
||||
originalRouterPath = openmct.router.path;
|
||||
|
||||
openmct.telemetry.addFormat({
|
||||
key: 'thumbnail',
|
||||
format: formatThumbnail
|
||||
});
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
@ -384,15 +403,32 @@ 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 target = imageTelemetry[5].url;
|
||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||
const thumbnailUrl = formatThumbnail(imageTelemetry[5].url);
|
||||
parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click();
|
||||
await Vue.nextTick();
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
@ -417,7 +453,7 @@ describe("The Imagery View Layouts", () => {
|
||||
|
||||
it("should show that an image is not new", async () => {
|
||||
await Vue.nextTick();
|
||||
const target = imageTelemetry[4].url;
|
||||
const target = formatThumbnail(imageTelemetry[4].url);
|
||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||
|
||||
await Vue.nextTick();
|
||||
|
@ -30,6 +30,7 @@ export default class LinkAction {
|
||||
this.priority = 7;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.transaction = null;
|
||||
}
|
||||
|
||||
appliesTo(objectPath) {
|
||||
@ -48,7 +49,9 @@ export default class LinkAction {
|
||||
}
|
||||
|
||||
onSave(changes) {
|
||||
let inNavigationPath = this.inNavigationPath();
|
||||
this.startTransaction();
|
||||
|
||||
const inNavigationPath = this.inNavigationPath();
|
||||
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
}
|
||||
@ -57,6 +60,8 @@ export default class LinkAction {
|
||||
const parent = parentDomainObjectpath[0];
|
||||
|
||||
this.linkInNewParent(this.object, parent);
|
||||
|
||||
return this.saveTransaction();
|
||||
}
|
||||
|
||||
linkInNewParent(child, newParent) {
|
||||
@ -128,4 +133,19 @@ 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,6 +29,7 @@ export default class MoveAction {
|
||||
this.priority = 7;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.transaction = null;
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
@ -60,6 +61,8 @@ 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();
|
||||
@ -81,6 +84,8 @@ export default class MoveAction {
|
||||
this.addToNewParent(this.object, parent);
|
||||
this.removeFromOldParent(this.object);
|
||||
|
||||
await this.saveTransaction();
|
||||
|
||||
if (!inNavigationPath) {
|
||||
return;
|
||||
}
|
||||
@ -189,4 +194,20 @@ 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,13 +25,14 @@ import Notebook from './components/Notebook.vue';
|
||||
import Agent from '@/utils/agent/Agent';
|
||||
|
||||
export default class NotebookViewProvider {
|
||||
constructor(openmct, name, key, type, cssClass, snapshotContainer) {
|
||||
constructor(openmct, name, key, type, cssClass, snapshotContainer, entryUrlWhitelist) {
|
||||
this.openmct = openmct;
|
||||
this.key = key;
|
||||
this.name = `${name} View`;
|
||||
this.type = type;
|
||||
this.cssClass = cssClass;
|
||||
this.snapshotContainer = snapshotContainer;
|
||||
this.entryUrlWhitelist = entryUrlWhitelist;
|
||||
}
|
||||
|
||||
canView(domainObject) {
|
||||
@ -43,6 +44,7 @@ export default class NotebookViewProvider {
|
||||
let openmct = this.openmct;
|
||||
let snapshotContainer = this.snapshotContainer;
|
||||
let agent = new Agent(window);
|
||||
let entryUrlWhitelist = this.entryUrlWhitelist;
|
||||
|
||||
return {
|
||||
show(container) {
|
||||
@ -54,7 +56,8 @@ export default class NotebookViewProvider {
|
||||
provide: {
|
||||
openmct,
|
||||
snapshotContainer,
|
||||
agent
|
||||
agent,
|
||||
entryUrlWhitelist
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -162,10 +162,12 @@
|
||||
: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
|
||||
@ -234,6 +236,7 @@ export default {
|
||||
sidebarCoversEntries: false,
|
||||
filteredAndSortedEntries: [],
|
||||
notebookAnnotations: {},
|
||||
selectedEntryId: '',
|
||||
activeTransaction: false,
|
||||
savingTransaction: false
|
||||
};
|
||||
@ -321,6 +324,7 @@ export default {
|
||||
this.formatSidebar();
|
||||
this.setSectionAndPageFromUrl();
|
||||
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.transaction = null;
|
||||
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
@ -346,6 +350,7 @@ export default {
|
||||
|
||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
},
|
||||
updated: function () {
|
||||
this.$nextTick(() => {
|
||||
@ -375,15 +380,20 @@ 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 query = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
const foundAnnotations = await this.openmct.annotation.getAnnotations(query);
|
||||
const foundAnnotations = await this.openmct.annotation.getAnnotations(this.domainObject.identifier);
|
||||
foundAnnotations.forEach((foundAnnotation) => {
|
||||
const targetId = Object.keys(foundAnnotation.targets)[0];
|
||||
const entryId = foundAnnotation.targets[targetId].entryId;
|
||||
@ -941,6 +951,9 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
entrySelection(entry) {
|
||||
this.selectedEntryId = entry.id;
|
||||
},
|
||||
endTransaction() {
|
||||
this.openmct.objects.endTransaction();
|
||||
this.transaction = null;
|
||||
|
@ -12,14 +12,15 @@
|
||||
<a
|
||||
class="c-ne__embed__link"
|
||||
:class="embed.cssClass"
|
||||
@click="changeLocation"
|
||||
@click="navigateToItemInTime"
|
||||
>{{ embed.name }}</a>
|
||||
<PopupMenu :popup-menu-items="popupMenuItems" />
|
||||
<button
|
||||
class="c-ne__embed__actions c-icon-button icon-3-dots"
|
||||
title="More options"
|
||||
@click.prevent.stop="showMenuItems($event)"
|
||||
></button>
|
||||
</div>
|
||||
<div
|
||||
v-if="embed.snapshot"
|
||||
class="c-ne__embed__time"
|
||||
>
|
||||
<div class="c-ne__embed__time">
|
||||
{{ createdOn }}
|
||||
</div>
|
||||
</div>
|
||||
@ -32,17 +33,14 @@ 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: {
|
||||
@ -72,7 +70,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
popupMenuItems: []
|
||||
menuActions: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -88,37 +86,88 @@ export default {
|
||||
watch: {
|
||||
isLocked(value) {
|
||||
if (value === true) {
|
||||
let index = this.popupMenuItems.findIndex((item) => item.id === 'removeEmbed');
|
||||
let index = this.menuActions.findIndex((item) => item.id === 'removeEmbed');
|
||||
|
||||
this.$delete(this.popupMenuItems, index);
|
||||
this.$delete(this.menuActions, index);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.addPopupMenuItems();
|
||||
async mounted() {
|
||||
this.objectPath = [];
|
||||
await this.setEmbedObjectPath();
|
||||
this.addMenuActions();
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
},
|
||||
methods: {
|
||||
addPopupMenuItems() {
|
||||
const removeEmbed = {
|
||||
id: 'removeEmbed',
|
||||
cssClass: 'icon-trash',
|
||||
name: this.removeActionString,
|
||||
callback: this.getRemoveDialog.bind(this)
|
||||
};
|
||||
const preview = {
|
||||
id: 'preview',
|
||||
cssClass: 'icon-eye-open',
|
||||
name: 'Preview',
|
||||
callback: this.previewEmbed.bind(this)
|
||||
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
|
||||
};
|
||||
|
||||
this.popupMenuItems = [preview];
|
||||
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()
|
||||
};
|
||||
|
||||
if (!this.isLocked) {
|
||||
this.popupMenuItems.unshift(removeEmbed);
|
||||
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',
|
||||
cssClass: 'icon-eye-open',
|
||||
name: 'Quick View',
|
||||
description: 'Full screen overlay view of the item.',
|
||||
onItemClicked: () => this.previewEmbed()
|
||||
};
|
||||
|
||||
this.menuActions = this.menuActions.concat([quickView, navigateToItem, navigateToItemInTime]);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
},
|
||||
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({
|
||||
@ -179,7 +228,11 @@ export default {
|
||||
painterroInstance.show(object.configuration.fullSizeImageURL);
|
||||
});
|
||||
},
|
||||
changeLocation() {
|
||||
navigateToItem() {
|
||||
const url = objectPathToUrl(this.openmct, this.objectPath);
|
||||
this.openmct.router.navigate(url);
|
||||
},
|
||||
navigateToItemInTime() {
|
||||
const hash = this.embed.historicLink;
|
||||
|
||||
const bounds = this.openmct.time.bounds();
|
||||
|
@ -1,3 +1,4 @@
|
||||
<!-- 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
|
||||
@ -22,23 +23,37 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-notebook__entry c-ne has-local-controls has-tag-applier"
|
||||
class="c-notebook__entry c-ne has-local-controls"
|
||||
aria-label="Notebook Entry"
|
||||
:class="{ 'locked': isLocked }"
|
||||
:class="{ 'locked': isLocked, 'is-selected': isSelectedEntry }"
|
||||
@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">
|
||||
<span class="c-ne__created-date">{{ createdOnDate }}</span>
|
||||
<span class="c-ne__created-time">{{ createdOnTime }}</span>
|
||||
|
||||
<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>
|
||||
<span
|
||||
v-if="entry.createdBy"
|
||||
class="c-ne__creator"
|
||||
v-if="!readOnly && !isLocked"
|
||||
class="c-ne__local-controls--hidden"
|
||||
>
|
||||
<span class="icon-person"></span> {{ entry.createdBy }}
|
||||
<button
|
||||
class="c-ne__remove c-icon-button c-icon-button--major icon-trash"
|
||||
title="Delete this entry"
|
||||
tabindex="-1"
|
||||
@click="deleteEntry"
|
||||
>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="c-ne__content">
|
||||
@ -61,12 +76,14 @@
|
||||
class="c-ne__text c-ne__input"
|
||||
aria-label="Notebook Entry Input"
|
||||
tabindex="0"
|
||||
contenteditable="true"
|
||||
:contenteditable="canEdit"
|
||||
@mouseover="checkEditability($event)"
|
||||
@mouseleave="canEdit = true"
|
||||
@focus="editingEntry()"
|
||||
@blur="updateEntryValue($event)"
|
||||
@keydown.enter.exact.prevent
|
||||
@keyup.enter.exact.prevent="forceBlur($event)"
|
||||
v-text="entry.text"
|
||||
v-html="formattedText"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
@ -77,43 +94,42 @@
|
||||
class="c-ne__text"
|
||||
contenteditable="false"
|
||||
tabindex="0"
|
||||
v-text="entry.text"
|
||||
v-html="formattedText"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<TagEditor
|
||||
:domain-object="domainObject"
|
||||
:annotations="notebookAnnotations"
|
||||
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
||||
:target-specific-details="{entryId: entry.id}"
|
||||
@tags-updated="timestampAndUpdate"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
v-for="(tag, index) in entryTags"
|
||||
:key="index"
|
||||
class="c-tag"
|
||||
:style="{ backgroundColor: tag.backgroundColor, color: tag.foregroundColor }"
|
||||
>
|
||||
{{ tag.label }}
|
||||
</div>
|
||||
</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
|
||||
: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>
|
||||
</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"
|
||||
@ -139,22 +155,28 @@
|
||||
|
||||
<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,
|
||||
TagEditor
|
||||
TextHighlight
|
||||
},
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
inject: ['openmct', 'snapshotContainer', 'entryUrlWhitelist'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@ -203,8 +225,19 @@ 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');
|
||||
@ -212,6 +245,39 @@ 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;
|
||||
|
||||
@ -232,7 +298,23 @@ 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) {
|
||||
@ -245,6 +327,8 @@ export default {
|
||||
};
|
||||
const newEmbed = await createNewEmbed(snapshotMeta);
|
||||
this.entry.embeds.push(newEmbed);
|
||||
|
||||
this.manageEmbedLayout();
|
||||
},
|
||||
cancelEditMode(event) {
|
||||
const isEditing = this.openmct.editor.isEditing();
|
||||
@ -262,9 +346,25 @@ 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();
|
||||
|
||||
@ -322,6 +422,8 @@ export default {
|
||||
this.entry.embeds.splice(embedPosition, 1);
|
||||
|
||||
this.timestampAndUpdate();
|
||||
|
||||
this.manageEmbedLayout();
|
||||
},
|
||||
updateEmbed(newEmbed) {
|
||||
this.entry.embeds.some(e => {
|
||||
@ -347,9 +449,11 @@ 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;
|
||||
@ -357,6 +461,38 @@ 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -103,7 +103,7 @@ function installBaseNotebookFunctionality(openmct) {
|
||||
monkeyPatchObjectAPIForNotebooks(openmct);
|
||||
}
|
||||
|
||||
function NotebookPlugin(name = 'Notebook') {
|
||||
function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {
|
||||
return function install(openmct) {
|
||||
if (openmct[NOTEBOOK_INSTALLED_KEY]) {
|
||||
return;
|
||||
@ -118,8 +118,8 @@ function NotebookPlugin(name = 'Notebook') {
|
||||
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);
|
||||
openmct.objectViews.addProvider(notebookView);
|
||||
const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist);
|
||||
openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);
|
||||
|
||||
installBaseNotebookFunctionality(openmct);
|
||||
|
||||
@ -127,7 +127,7 @@ function NotebookPlugin(name = 'Notebook') {
|
||||
};
|
||||
}
|
||||
|
||||
function RestrictedNotebookPlugin(name = 'Notebook Shift Log') {
|
||||
function RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist = []) {
|
||||
return function install(openmct) {
|
||||
if (openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) {
|
||||
return;
|
||||
@ -140,8 +140,8 @@ function RestrictedNotebookPlugin(name = 'Notebook Shift Log') {
|
||||
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);
|
||||
openmct.objectViews.addProvider(notebookView);
|
||||
const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist);
|
||||
openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);
|
||||
|
||||
installBaseNotebookFunctionality(openmct);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="notifications.length > 0"
|
||||
v-if="notifications.length === 0 ? showNotificationsOverlay : notifications.length > 0"
|
||||
class="c-indicator c-indicator--clickable icon-bell"
|
||||
:class="[severityClass]"
|
||||
>
|
||||
|
@ -88,6 +88,14 @@
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
[class*='__label'] {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
[class*='__poll-table'] {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
[class*='new-question'] {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@ -123,6 +131,12 @@
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
&__actions {
|
||||
display:flex;
|
||||
flex: auto;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.c-indicator {
|
||||
|
@ -58,6 +58,13 @@
|
||||
{{ 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>
|
||||
|
||||
@ -74,6 +81,41 @@
|
||||
@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>
|
||||
@ -97,9 +139,11 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
pollQuestionUpdated: '--',
|
||||
pollQuestionTimestamp: undefined,
|
||||
currentPollQuestion: '--',
|
||||
newPollQuestion: undefined,
|
||||
statusCountViewModel: []
|
||||
statusCountViewModel: [],
|
||||
statusesForRolesViewModel: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -135,9 +179,17 @@ export default {
|
||||
this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
|
||||
},
|
||||
setPollQuestion(pollQuestion) {
|
||||
this.currentPollQuestion = pollQuestion.question;
|
||||
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.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();
|
||||
this.indicator.text(pollQuestion.question);
|
||||
},
|
||||
async updatePollQuestion() {
|
||||
const result = await this.openmct.user.status.setPollQuestion(this.newPollQuestion);
|
||||
@ -149,6 +201,13 @@ 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) => {
|
||||
@ -158,7 +217,6 @@ 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;
|
||||
@ -170,6 +228,51 @@ 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("Poll Question");
|
||||
pollQuestionIndicator.text("No Poll Question");
|
||||
pollQuestionIndicator.description("Set the current poll question");
|
||||
pollQuestionIndicator.iconClass('icon-status-poll-edit');
|
||||
pollQuestionIndicator.element.classList.add("c-indicator--operator-status");
|
||||
|
@ -103,6 +103,12 @@ export default {
|
||||
return 6;
|
||||
}
|
||||
},
|
||||
axisId: {
|
||||
type: Number,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
position: {
|
||||
required: true,
|
||||
type: String,
|
||||
@ -145,7 +151,15 @@ export default {
|
||||
throw new Error('config is missing');
|
||||
}
|
||||
|
||||
return config[this.axisType];
|
||||
if (this.axisType === 'yAxis') {
|
||||
if (this.axisId && this.axisId !== config.yAxis.id) {
|
||||
return config.additionalYAxes.find(axis => axis.id === this.axisId);
|
||||
} else {
|
||||
return config.yAxis;
|
||||
}
|
||||
} else {
|
||||
return config[this.axisType];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determine whether ticks should be regenerated for a given range.
|
||||
@ -258,7 +272,10 @@ export default {
|
||||
}, 0));
|
||||
|
||||
this.tickWidth = tickWidth;
|
||||
this.$emit('plotTickWidth', tickWidth);
|
||||
this.$emit('plotTickWidth', {
|
||||
width: tickWidth,
|
||||
yAxisId: this.axisType === 'yAxis' ? this.axisId : ''
|
||||
});
|
||||
this.shouldCheckWidth = false;
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
<div
|
||||
ref="plotWrapper"
|
||||
class="c-plot holder holder-plot has-control-bar"
|
||||
:class="staleClass"
|
||||
>
|
||||
<div
|
||||
ref="plotContainer"
|
||||
@ -50,6 +51,7 @@ 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: {
|
||||
@ -74,21 +76,95 @@ export default {
|
||||
cursorGuide: false,
|
||||
gridLines: !this.options.compact,
|
||||
loading: false,
|
||||
status: ''
|
||||
status: '',
|
||||
staleObjects: []
|
||||
};
|
||||
},
|
||||
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() {
|
||||
|
@ -22,19 +22,28 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="gl-plot-axis-area gl-plot-y has-local-controls"
|
||||
:style="{
|
||||
width: (tickWidth + 20) + 'px'
|
||||
}"
|
||||
class="gl-plot-axis-area gl-plot-y has-local-controls js-plot-y-axis"
|
||||
:style="yAxisStyle"
|
||||
>
|
||||
|
||||
<div
|
||||
v-if="canShowYAxisLabel"
|
||||
class="gl-plot-label gl-plot-y-label"
|
||||
:class="{'icon-gear': (yKeyOptions.length > 1 && singleSeries)}"
|
||||
>{{ yAxisLabel }}
|
||||
>
|
||||
<span
|
||||
v-for="(colorAsHexString, index) in seriesColors"
|
||||
:key="`${colorAsHexString}-${index}`"
|
||||
class="plot-series-color-swatch"
|
||||
:style="{ 'background-color': colorAsHexString }"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
:class="{'icon-gear-after': (yKeyOptions.length > 1 && singleSeries)}"
|
||||
>{{ canShowYAxisLabel ? yAxisLabel : `Y Axis ${id}` }}</span>
|
||||
<span
|
||||
v-if="showVisibilityToggle"
|
||||
:class="{ 'icon-eye-open': visible, 'icon-eye-disabled': !visible}"
|
||||
@click="toggleSeriesVisibility"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<select
|
||||
v-if="yKeyOptions.length > 1 && singleSeries"
|
||||
v-model="yAxisLabel"
|
||||
@ -52,6 +61,7 @@
|
||||
</select>
|
||||
|
||||
<mct-ticks
|
||||
:axis-id="id"
|
||||
:axis-type="'yAxis'"
|
||||
class="gl-plot-ticks"
|
||||
:position="'top'"
|
||||
@ -63,6 +73,9 @@
|
||||
<script>
|
||||
import MctTicks from "../MctTicks.vue";
|
||||
import configStore from "../configuration/ConfigStore";
|
||||
import eventHelpers from "../lib/eventHelpers";
|
||||
|
||||
const AXIS_PADDING = 20;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -70,22 +83,10 @@ export default {
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
singleSeries: {
|
||||
type: Boolean,
|
||||
id: {
|
||||
type: Number,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
hasSameRangeValue: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
seriesModel: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
tickWidth: {
|
||||
@ -93,37 +94,141 @@ export default {
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
plotLeftTickWidth: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
multipleLeftAxes: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
position: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'left';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
yAxisLabel: 'none',
|
||||
loaded: false
|
||||
loaded: false,
|
||||
yKeyOptions: [],
|
||||
hasSameRangeValue: true,
|
||||
singleSeries: true,
|
||||
mainYAxisId: null,
|
||||
hasAdditionalYAxes: false,
|
||||
seriesColors: [],
|
||||
visible: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showVisibilityToggle() {
|
||||
return this.domainObject.type === 'telemetry.plot.overlay';
|
||||
},
|
||||
canShowYAxisLabel() {
|
||||
return this.singleSeries === true || this.hasSameRangeValue === true;
|
||||
},
|
||||
yAxisStyle() {
|
||||
let style = {
|
||||
width: `${this.tickWidth + AXIS_PADDING}px`
|
||||
};
|
||||
const multipleAxesPadding = this.multipleLeftAxes ? AXIS_PADDING : 0;
|
||||
|
||||
if (this.position === 'right') {
|
||||
style.left = `-${this.tickWidth + AXIS_PADDING}px`;
|
||||
} else {
|
||||
const thisIsTheSecondLeftAxis = (this.id - 1) > 0;
|
||||
if (this.multipleLeftAxes && thisIsTheSecondLeftAxis) {
|
||||
style.left = 0;
|
||||
style['border-right'] = `1px solid`;
|
||||
} else {
|
||||
style.left = `${ this.plotLeftTickWidth - this.tickWidth + multipleAxesPadding}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.yAxis = this.getYAxisFromConfig();
|
||||
this.seriesModels = [];
|
||||
eventHelpers.extend(this);
|
||||
this.initAxisAndSeriesConfig();
|
||||
this.loaded = true;
|
||||
this.setUpYAxisOptions();
|
||||
},
|
||||
methods: {
|
||||
getYAxisFromConfig() {
|
||||
initAxisAndSeriesConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
if (config) {
|
||||
return config.yAxis;
|
||||
this.mainYAxisId = config.yAxis.id;
|
||||
this.hasAdditionalYAxes = config?.additionalYAxes.length;
|
||||
if (this.id && this.id !== this.mainYAxisId) {
|
||||
this.yAxis = config.additionalYAxes.find(yAxis => yAxis.id === this.id);
|
||||
} else {
|
||||
this.yAxis = config.yAxis;
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
this.listenTo(this.config.series, 'reorder', this.addOrRemoveSeries, this);
|
||||
|
||||
this.config.series.models.forEach(this.addSeries, this);
|
||||
}
|
||||
},
|
||||
addOrRemoveSeries(series) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
if (yAxisId === this.id) {
|
||||
this.addSeries(series);
|
||||
} else {
|
||||
this.removeSeries(series);
|
||||
}
|
||||
},
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), series.get('identifier')));
|
||||
|
||||
if (yAxisId === this.id && seriesIndex < 0) {
|
||||
this.seriesModels.push(series);
|
||||
this.processSeries();
|
||||
this.setUpYAxisOptions();
|
||||
}
|
||||
|
||||
this.listenTo(series, 'change:yAxisId', this.addOrRemoveSeries.bind(this, series), this);
|
||||
},
|
||||
removeSeries(plotSeries) {
|
||||
const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), plotSeries.get('identifier')));
|
||||
if (seriesIndex > -1) {
|
||||
this.seriesModels.splice(seriesIndex, 1);
|
||||
this.processSeries();
|
||||
this.setUpYAxisOptions();
|
||||
}
|
||||
},
|
||||
processSeries() {
|
||||
this.hasSameRangeValue = this.seriesModels.every((model) => {
|
||||
return model.get('yKey') === this.seriesModels[0].get('yKey');
|
||||
});
|
||||
this.singleSeries = this.seriesModels.length === 1;
|
||||
this.seriesColors = this.seriesModels.map(model => {
|
||||
return model.get('color').asHexString();
|
||||
});
|
||||
},
|
||||
setUpYAxisOptions() {
|
||||
this.yKeyOptions = [];
|
||||
if (!this.seriesModels.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.seriesModel.metadata) {
|
||||
this.yKeyOptions = this.seriesModel.metadata
|
||||
const seriesModel = this.seriesModels[0];
|
||||
if (seriesModel.metadata) {
|
||||
this.yKeyOptions = seriesModel.metadata
|
||||
.valuesForHints(['range'])
|
||||
.map(function (o) {
|
||||
return {
|
||||
@ -135,22 +240,29 @@ export default {
|
||||
|
||||
// set yAxisLabel if none is set yet
|
||||
if (this.yAxisLabel === 'none') {
|
||||
let yKey = this.seriesModel.model.yKey;
|
||||
let yKeyModel = this.yKeyOptions.filter(o => o.key === yKey)[0];
|
||||
|
||||
this.yAxisLabel = yKeyModel ? yKeyModel.name : '';
|
||||
this.yAxisLabel = this.yAxis.get('label');
|
||||
}
|
||||
},
|
||||
toggleYAxisLabel() {
|
||||
let yAxisObject = this.yKeyOptions.filter(o => o.name === this.yAxisLabel)[0];
|
||||
|
||||
if (yAxisObject) {
|
||||
this.$emit('yKeyChanged', yAxisObject.key);
|
||||
this.$emit('yKeyChanged', yAxisObject.key, this.id);
|
||||
this.yAxis.set('label', this.yAxisLabel);
|
||||
}
|
||||
},
|
||||
onTickWidthChange(width) {
|
||||
this.$emit('tickWidthChanged', width);
|
||||
onTickWidthChange(data) {
|
||||
this.$emit('tickWidthChanged', {
|
||||
width: data.width,
|
||||
yAxisId: this.id
|
||||
});
|
||||
},
|
||||
toggleSeriesVisibility() {
|
||||
this.visible = !this.visible;
|
||||
this.$emit('toggleAxisVisibility', {
|
||||
id: this.id,
|
||||
visible: this.visible
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -105,6 +105,9 @@ export default class MCTChartAlarmLineSet {
|
||||
|
||||
reset() {
|
||||
this.limits = [];
|
||||
if (this.series.limits) {
|
||||
this.getLimitPoints(this.series);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
@ -50,10 +50,11 @@ import Vue from 'vue';
|
||||
|
||||
const MARKER_SIZE = 6.0;
|
||||
const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;
|
||||
const ANNOTATION_SIZE = MARKER_SIZE * 3.0;
|
||||
const CLEARANCE = 15;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
props: {
|
||||
rectangles: {
|
||||
type: Array,
|
||||
@ -67,11 +68,33 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
annotatedPoints: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
annotationSelections: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
showLimitLineLabels: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
hiddenYAxisIds: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
annotationViewingAndEditingAllowed: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -83,11 +106,24 @@ export default {
|
||||
highlights() {
|
||||
this.scheduleDraw();
|
||||
},
|
||||
annotatedPoints() {
|
||||
this.scheduleDraw();
|
||||
},
|
||||
annotationSelections() {
|
||||
this.scheduleDraw();
|
||||
},
|
||||
rectangles() {
|
||||
this.scheduleDraw();
|
||||
},
|
||||
showLimitLineLabels() {
|
||||
this.drawLimitLines();
|
||||
},
|
||||
hiddenYAxisIds() {
|
||||
this.hiddenYAxisIds.forEach(id => {
|
||||
this.resetYOffsetAndSeriesDataForYAxis(id);
|
||||
this.drawLimitLines();
|
||||
});
|
||||
this.scheduleDraw();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -98,7 +134,21 @@ export default {
|
||||
this.limitLines = [];
|
||||
this.pointSets = [];
|
||||
this.alarmSets = [];
|
||||
this.offset = {};
|
||||
const yAxisId = this.config.yAxis.get('id');
|
||||
this.offset = {
|
||||
[yAxisId]: {}
|
||||
};
|
||||
this.listenTo(this.config.yAxis, 'change:key', this.resetYOffsetAndSeriesDataForYAxis.bind(this, yAxisId), this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw);
|
||||
if (this.config.additionalYAxes.length) {
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
const id = yAxis.get('id');
|
||||
this.offset[id] = {};
|
||||
this.listenTo(yAxis, 'change', this.updateLimitsAndDraw);
|
||||
this.listenTo(yAxis, 'change:key', this.resetYOffsetAndSeriesDataForYAxis.bind(this, id), this);
|
||||
});
|
||||
}
|
||||
|
||||
this.seriesElements = new WeakMap();
|
||||
this.seriesLimits = new WeakMap();
|
||||
|
||||
@ -111,8 +161,7 @@ export default {
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
|
||||
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
|
||||
this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw);
|
||||
|
||||
this.listenTo(this.config.xAxis, 'change', this.updateLimitsAndDraw);
|
||||
this.config.series.forEach(this.onSeriesAdd, this);
|
||||
this.$emit('chartLoaded');
|
||||
@ -147,11 +196,36 @@ export default {
|
||||
this.listenTo(series, 'change:markers', this.changeMarkers, this);
|
||||
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
|
||||
this.listenTo(series, 'change:limitLines', this.changeLimitLines, this);
|
||||
this.listenTo(series, 'change:yAxisId', this.resetAxisAndRedraw, this);
|
||||
// TODO: Which other changes is the listener below reacting to?
|
||||
this.listenTo(series, 'change', this.scheduleDraw);
|
||||
this.listenTo(series, 'add', this.scheduleDraw);
|
||||
this.listenTo(series, 'add', this.onAddPoint);
|
||||
this.makeChartElement(series);
|
||||
this.makeLimitLines(series);
|
||||
},
|
||||
onAddPoint(point, insertIndex, series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const seriesYAxisId = series.get('yAxisId');
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
|
||||
let yRange;
|
||||
if (seriesYAxisId === mainYAxisId) {
|
||||
yRange = this.config.yAxis.get('displayRange');
|
||||
} else {
|
||||
yRange = this.config.additionalYAxes.find(
|
||||
yAxis => yAxis.get('id') === seriesYAxisId
|
||||
).get('displayRange');
|
||||
}
|
||||
|
||||
const xValue = series.getXVal(point);
|
||||
const yValue = series.getYVal(point);
|
||||
|
||||
// if user is not looking at data within the current bounds, don't draw the point
|
||||
if ((xValue > xRange.min) && (xValue < xRange.max)
|
||||
&& (yValue > yRange.min) && (yValue < yRange.max)) {
|
||||
this.scheduleDraw();
|
||||
}
|
||||
},
|
||||
changeInterpolate(mode, o, series) {
|
||||
if (mode === o) {
|
||||
return;
|
||||
@ -212,6 +286,21 @@ export default {
|
||||
this.makeLimitLines(series);
|
||||
this.updateLimitsAndDraw();
|
||||
},
|
||||
resetAxisAndRedraw(newYAxisId, oldYAxisId, series) {
|
||||
if (!oldYAxisId) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Remove the old chart elements for the series since their offsets are pointing to the old y axis
|
||||
this.removeChartElement(series);
|
||||
this.resetYOffsetAndSeriesDataForYAxis(oldYAxisId);
|
||||
|
||||
//Make the chart elements again for the new y-axis and offset
|
||||
this.makeChartElement(series);
|
||||
this.makeLimitLines(series);
|
||||
|
||||
this.scheduleDraw();
|
||||
},
|
||||
onSeriesRemove(series) {
|
||||
this.stopListening(series);
|
||||
this.removeChartElement(series);
|
||||
@ -224,25 +313,33 @@ export default {
|
||||
this.limitLines.forEach(line => line.destroy());
|
||||
DrawLoader.releaseDrawAPI(this.drawAPI);
|
||||
},
|
||||
clearOffset() {
|
||||
delete this.offset.x;
|
||||
delete this.offset.y;
|
||||
delete this.offset.xVal;
|
||||
delete this.offset.yVal;
|
||||
delete this.offset.xKey;
|
||||
delete this.offset.yKey;
|
||||
this.lines.forEach(function (line) {
|
||||
resetYOffsetAndSeriesDataForYAxis(yAxisId) {
|
||||
delete this.offset[yAxisId].y;
|
||||
delete this.offset[yAxisId].xVal;
|
||||
delete this.offset[yAxisId].yVal;
|
||||
delete this.offset[yAxisId].xKey;
|
||||
delete this.offset[yAxisId].yKey;
|
||||
|
||||
this.resetResetChartElements(yAxisId);
|
||||
},
|
||||
resetResetChartElements(yAxisId) {
|
||||
const lines = this.lines.filter(this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId));
|
||||
lines.forEach(function (line) {
|
||||
line.reset();
|
||||
});
|
||||
this.limitLines.forEach(function (line) {
|
||||
const limitLines = this.limitLines.filter(this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId));
|
||||
limitLines.forEach(function (line) {
|
||||
line.reset();
|
||||
});
|
||||
this.pointSets.forEach(function (pointSet) {
|
||||
const pointSets = this.pointSets.filter(this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId));
|
||||
pointSets.forEach(function (pointSet) {
|
||||
pointSet.reset();
|
||||
});
|
||||
},
|
||||
setOffset(offsetPoint, index, series) {
|
||||
if (this.offset.x && this.offset.y) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
if (this.offset[yAxisId].x && this.offset[yAxisId].y) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -251,19 +348,20 @@ export default {
|
||||
y: series.getYVal(offsetPoint)
|
||||
};
|
||||
|
||||
this.offset.x = function (x) {
|
||||
this.offset[yAxisId].x = function (x) {
|
||||
return x - offsets.x;
|
||||
}.bind(this);
|
||||
this.offset.y = function (y) {
|
||||
this.offset[yAxisId].y = function (y) {
|
||||
return y - offsets.y;
|
||||
}.bind(this);
|
||||
this.offset.xVal = function (point, pSeries) {
|
||||
return this.offset.x(pSeries.getXVal(point));
|
||||
this.offset[yAxisId].xVal = function (point, pSeries) {
|
||||
return this.offset[yAxisId].x(pSeries.getXVal(point));
|
||||
}.bind(this);
|
||||
this.offset.yVal = function (point, pSeries) {
|
||||
return this.offset.y(pSeries.getYVal(point));
|
||||
this.offset[yAxisId].yVal = function (point, pSeries) {
|
||||
return this.offset[yAxisId].y(pSeries.getYVal(point));
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
initializeCanvas(canvas, overlay) {
|
||||
this.canvas = canvas;
|
||||
this.overlay = overlay;
|
||||
@ -311,11 +409,15 @@ export default {
|
||||
this.clearLimitLines(series);
|
||||
},
|
||||
lineForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
if (series.get('interpolate') === 'linear') {
|
||||
return new MCTChartLineLinear(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
|
||||
@ -323,33 +425,45 @@ export default {
|
||||
return new MCTChartLineStepAfter(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
},
|
||||
limitLineForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
return new MCTChartAlarmLineSet(
|
||||
series,
|
||||
this,
|
||||
this.offset,
|
||||
offset,
|
||||
this.openmct.time.bounds()
|
||||
);
|
||||
},
|
||||
pointSetForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
if (series.get('markers')) {
|
||||
return new MCTChartPointSet(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
},
|
||||
alarmPointSetForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
if (series.get('alarmMarkers')) {
|
||||
return new MCTChartAlarmPointSet(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -410,8 +524,8 @@ export default {
|
||||
this.seriesLimits.delete(series);
|
||||
}
|
||||
},
|
||||
canDraw() {
|
||||
if (!this.offset.x || !this.offset.y) {
|
||||
canDraw(yAxisId) {
|
||||
if (!this.offset[yAxisId] || !this.offset[yAxisId].x || !this.offset[yAxisId].y) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -434,16 +548,37 @@ export default {
|
||||
}
|
||||
|
||||
this.drawAPI.clear();
|
||||
if (this.canDraw()) {
|
||||
this.updateViewport();
|
||||
this.drawSeries();
|
||||
this.drawRectangles();
|
||||
this.drawHighlights();
|
||||
}
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
//There has to be at least one yAxis
|
||||
const yAxisIds = [mainYAxisId].concat(this.config.additionalYAxes.map(yAxis => yAxis.get('id')));
|
||||
// Repeat drawing for all yAxes
|
||||
yAxisIds.forEach((id) => {
|
||||
if (this.canDraw(id)) {
|
||||
this.updateViewport(id);
|
||||
this.drawSeries(id);
|
||||
this.drawRectangles(id);
|
||||
this.drawHighlights(id);
|
||||
|
||||
// only draw these in fixed time mode or plot is paused
|
||||
if (this.annotationViewingAndEditingAllowed) {
|
||||
this.drawAnnotatedPoints(id);
|
||||
this.drawAnnotationSelections(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
updateViewport() {
|
||||
updateViewport(yAxisId) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
let yRange;
|
||||
if (yAxisId === mainYAxisId) {
|
||||
yRange = this.config.yAxis.get('displayRange');
|
||||
} else {
|
||||
if (this.config.additionalYAxes.length) {
|
||||
const yAxisForId = this.config.additionalYAxes.find(yAxis => yAxis.get('id') === yAxisId);
|
||||
yRange = yAxisForId.get('displayRange');
|
||||
}
|
||||
}
|
||||
|
||||
if (!xRange || !yRange) {
|
||||
return;
|
||||
@ -454,9 +589,10 @@ export default {
|
||||
yRange.max - yRange.min
|
||||
];
|
||||
|
||||
const origin = [
|
||||
this.offset.x(xRange.min),
|
||||
this.offset.y(yRange.min)
|
||||
let origin;
|
||||
origin = [
|
||||
this.offset[yAxisId].x(xRange.min),
|
||||
this.offset[yAxisId].y(yRange.min)
|
||||
];
|
||||
|
||||
this.drawAPI.setDimensions(
|
||||
@ -464,38 +600,74 @@ export default {
|
||||
origin
|
||||
);
|
||||
},
|
||||
drawSeries() {
|
||||
this.lines.forEach(this.drawLine, this);
|
||||
this.pointSets.forEach(this.drawPoints, this);
|
||||
this.alarmSets.forEach(this.drawAlarmPoints, this);
|
||||
// match items by their yAxisId, but don't care if the series is hidden or not.
|
||||
matchByYAxisIdExcludingVisibility() {
|
||||
const args = Array.from(arguments).slice(0, 4);
|
||||
|
||||
return this.matchByYAxisId(...args, true);
|
||||
},
|
||||
matchByYAxisId(id, item, index, items, excludeVisibility = false) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
let matchesId = false;
|
||||
const axisSeriesAreVisible = excludeVisibility || this.hiddenYAxisIds.indexOf(id) < 0;
|
||||
const series = item.series;
|
||||
if (axisSeriesAreVisible && series) {
|
||||
const seriesYAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
matchesId = seriesYAxisId === id;
|
||||
}
|
||||
|
||||
return matchesId;
|
||||
},
|
||||
drawSeries(id) {
|
||||
const lines = this.lines.filter(this.matchByYAxisId.bind(this, id));
|
||||
lines.forEach(this.drawLine, this);
|
||||
const pointSets = this.pointSets.filter(this.matchByYAxisId.bind(this, id));
|
||||
pointSets.forEach(this.drawPoints, this);
|
||||
const alarmSets = this.alarmSets.filter(this.matchByYAxisId.bind(this, id));
|
||||
alarmSets.forEach(this.drawAlarmPoints, this);
|
||||
},
|
||||
drawLimitLines() {
|
||||
if (this.canDraw()) {
|
||||
this.updateViewport();
|
||||
Array.from(this.$refs.limitArea.children).forEach((el) => el.remove());
|
||||
this.config.series.models.forEach(series => {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
|
||||
if (!this.drawAPI.origin) {
|
||||
return;
|
||||
if (this.hiddenYAxisIds.indexOf(yAxisId) < 0) {
|
||||
this.drawLimitLinesForSeries(yAxisId, series);
|
||||
}
|
||||
|
||||
Array.from(this.$refs.limitArea.children).forEach((el) => el.remove());
|
||||
let limitPointOverlap = [];
|
||||
this.limitLines.forEach((limitLine) => {
|
||||
let limitContainerEl = this.$refs.limitArea;
|
||||
limitLine.limits.forEach((limit) => {
|
||||
const showLabels = this.showLabels(limit.seriesKey);
|
||||
if (showLabels) {
|
||||
const overlap = this.getLimitOverlap(limit, limitPointOverlap);
|
||||
limitPointOverlap.push(overlap);
|
||||
let limitLabelEl = this.getLimitLabel(limit, overlap);
|
||||
limitContainerEl.appendChild(limitLabelEl);
|
||||
}
|
||||
|
||||
let limitEl = this.getLimitElement(limit);
|
||||
limitContainerEl.appendChild(limitEl);
|
||||
|
||||
}, this);
|
||||
});
|
||||
});
|
||||
},
|
||||
drawLimitLinesForSeries(yAxisId, series) {
|
||||
if (!this.canDraw(yAxisId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateViewport(yAxisId);
|
||||
|
||||
if (!this.drawAPI.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
let limitPointOverlap = [];
|
||||
this.limitLines.forEach((limitLine) => {
|
||||
let limitContainerEl = this.$refs.limitArea;
|
||||
limitLine.limits.forEach((limit) => {
|
||||
if (series.keyString !== limit.seriesKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showLabels = this.showLabels(limit.seriesKey);
|
||||
if (showLabels) {
|
||||
const overlap = this.getLimitOverlap(limit, limitPointOverlap);
|
||||
limitPointOverlap.push(overlap);
|
||||
let limitLabelEl = this.getLimitLabel(limit, overlap);
|
||||
limitContainerEl.appendChild(limitLabelEl);
|
||||
}
|
||||
|
||||
let limitEl = this.getLimitElement(limit);
|
||||
limitContainerEl.appendChild(limitEl);
|
||||
|
||||
}, this);
|
||||
});
|
||||
},
|
||||
showLabels(seriesKey) {
|
||||
return this.showLimitLineLabels.seriesKey
|
||||
@ -577,22 +749,97 @@ export default {
|
||||
);
|
||||
},
|
||||
drawLine(chartElement, disconnected) {
|
||||
this.drawAPI.drawLine(
|
||||
chartElement.getBuffer(),
|
||||
chartElement.color().asRGBAArray(),
|
||||
chartElement.count,
|
||||
disconnected
|
||||
);
|
||||
},
|
||||
drawHighlights() {
|
||||
if (this.highlights && this.highlights.length) {
|
||||
this.highlights.forEach(this.drawHighlight, this);
|
||||
if (chartElement) {
|
||||
this.drawAPI.drawLine(
|
||||
chartElement.getBuffer(),
|
||||
chartElement.color().asRGBAArray(),
|
||||
chartElement.count,
|
||||
disconnected
|
||||
);
|
||||
}
|
||||
},
|
||||
drawHighlight(highlight) {
|
||||
annotatedPointWithinRange(annotatedPoint, xRange, yRange) {
|
||||
if (!yRange) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const xValue = annotatedPoint.series.getXVal(annotatedPoint.point);
|
||||
const yValue = annotatedPoint.series.getYVal(annotatedPoint.point);
|
||||
|
||||
return ((xValue > xRange.min) && (xValue < xRange.max)
|
||||
&& (yValue > yRange.min) && (yValue < yRange.max));
|
||||
},
|
||||
drawAnnotatedPoints(yAxisId) {
|
||||
// we should do this by series, and then plot all the points at once instead
|
||||
// of doing it one by one
|
||||
if (this.annotatedPoints && this.annotatedPoints.length) {
|
||||
const uniquePointsToDraw = [];
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
let yRange;
|
||||
if (yAxisId === this.config.yAxis.get('id')) {
|
||||
yRange = this.config.yAxis.get('displayRange');
|
||||
} else if (this.config.additionalYAxes.length) {
|
||||
const yAxisForId = this.config.additionalYAxes.find(yAxis => yAxis.get('id') === yAxisId);
|
||||
yRange = yAxisForId.get('displayRange');
|
||||
}
|
||||
|
||||
const annotatedPoints = this.annotatedPoints.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
annotatedPoints.forEach((annotatedPoint) => {
|
||||
// if the annotation is outside the range, don't draw it
|
||||
if (this.annotatedPointWithinRange(annotatedPoint, xRange, yRange)) {
|
||||
const canvasXValue = this.offset[yAxisId].xVal(annotatedPoint.point, annotatedPoint.series);
|
||||
const canvasYValue = this.offset[yAxisId].yVal(annotatedPoint.point, annotatedPoint.series);
|
||||
const pointToDraw = new Float32Array([canvasXValue, canvasYValue]);
|
||||
const drawnPoint = uniquePointsToDraw.some((rawPoint) => {
|
||||
return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1];
|
||||
});
|
||||
if (!drawnPoint) {
|
||||
uniquePointsToDraw.push(pointToDraw);
|
||||
this.drawAnnotatedPoint(annotatedPoint, pointToDraw);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
drawAnnotatedPoint(annotatedPoint, pointToDraw) {
|
||||
if (annotatedPoint.point && annotatedPoint.series) {
|
||||
const color = annotatedPoint.series.get('color').asRGBAArray();
|
||||
// set transparency
|
||||
color[3] = 0.15;
|
||||
const pointCount = 1;
|
||||
const shape = annotatedPoint.series.get('markerShape');
|
||||
|
||||
this.drawAPI.drawPoints(pointToDraw, color, pointCount, ANNOTATION_SIZE, shape);
|
||||
}
|
||||
},
|
||||
drawAnnotationSelections(yAxisId) {
|
||||
if (this.annotationSelections && this.annotationSelections.length) {
|
||||
const annotationSelections = this.annotationSelections.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
annotationSelections.forEach(this.drawAnnotationSelection.bind(this, yAxisId), this);
|
||||
}
|
||||
},
|
||||
drawAnnotationSelection(yAxisId, annotationSelection) {
|
||||
const points = new Float32Array([
|
||||
this.offset.xVal(highlight.point, highlight.series),
|
||||
this.offset.yVal(highlight.point, highlight.series)
|
||||
this.offset[yAxisId].xVal(annotationSelection.point, annotationSelection.series),
|
||||
this.offset[yAxisId].yVal(annotationSelection.point, annotationSelection.series)
|
||||
]);
|
||||
|
||||
const color = [255, 255, 255, 1]; // white
|
||||
const pointCount = 1;
|
||||
const shape = annotationSelection.series.get('markerShape');
|
||||
|
||||
this.drawAPI.drawPoints(points, color, pointCount, ANNOTATION_SIZE, shape);
|
||||
},
|
||||
drawHighlights(yAxisId) {
|
||||
if (this.highlights && this.highlights.length) {
|
||||
const highlights = this.highlights.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
highlights.forEach(this.drawHighlight.bind(this, yAxisId), this);
|
||||
}
|
||||
},
|
||||
drawHighlight(yAxisId, highlight) {
|
||||
const points = new Float32Array([
|
||||
this.offset[yAxisId].xVal(highlight.point, highlight.series),
|
||||
this.offset[yAxisId].yVal(highlight.point, highlight.series)
|
||||
]);
|
||||
|
||||
const color = highlight.series.get('color').asRGBAArray();
|
||||
@ -601,23 +848,31 @@ export default {
|
||||
|
||||
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
|
||||
},
|
||||
drawRectangles() {
|
||||
drawRectangles(yAxisId) {
|
||||
if (this.rectangles) {
|
||||
this.rectangles.forEach(this.drawRectangle, this);
|
||||
this.rectangles.forEach(this.drawRectangle.bind(this, yAxisId), this);
|
||||
}
|
||||
},
|
||||
drawRectangle(rect) {
|
||||
this.drawAPI.drawSquare(
|
||||
[
|
||||
this.offset.x(rect.start.x),
|
||||
this.offset.y(rect.start.y)
|
||||
],
|
||||
[
|
||||
this.offset.x(rect.end.x),
|
||||
this.offset.y(rect.end.y)
|
||||
],
|
||||
rect.color
|
||||
);
|
||||
drawRectangle(yAxisId, rect) {
|
||||
if (!rect.start.yAxisIds || !rect.end.yAxisIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startYIndex = rect.start.yAxisIds.findIndex(id => id === yAxisId);
|
||||
const endYIndex = rect.end.yAxisIds.findIndex(id => id === yAxisId);
|
||||
if (rect.start.y[startYIndex] && rect.end.y[endYIndex]) {
|
||||
this.drawAPI.drawSquare(
|
||||
[
|
||||
this.offset[yAxisId].x(rect.start.x),
|
||||
this.offset[yAxisId].y(rect.start.y[startYIndex])
|
||||
],
|
||||
[
|
||||
this.offset[yAxisId].x(rect.end.x),
|
||||
this.offset[yAxisId].y(rect.end.y[endYIndex])
|
||||
],
|
||||
rect.color
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -27,9 +27,13 @@ import XAxisModel from "./XAxisModel";
|
||||
import YAxisModel from "./YAxisModel";
|
||||
import LegendModel from "./LegendModel";
|
||||
|
||||
const MAX_Y_AXES = 3;
|
||||
const MAIN_Y_AXES_ID = 1;
|
||||
const MAX_ADDITIONAL_AXES = MAX_Y_AXES - 1;
|
||||
|
||||
/**
|
||||
* PlotConfiguration model stores the configuration of a plot and some
|
||||
* limited state. The indiidual parts of the plot configuration model
|
||||
* limited state. The individual parts of the plot configuration model
|
||||
* handle setting defaults and updating in response to various changes.
|
||||
*
|
||||
* @extends {Model<PlotConfigModelType, PlotConfigModelOptions>}
|
||||
@ -58,8 +62,34 @@ export default class PlotConfigurationModel extends Model {
|
||||
this.yAxis = new YAxisModel({
|
||||
model: options.model.yAxis,
|
||||
plot: this,
|
||||
openmct: options.openmct
|
||||
openmct: options.openmct,
|
||||
id: options.model.yAxis.id || MAIN_Y_AXES_ID
|
||||
});
|
||||
//Add any axes in addition to the main yAxis above - we must always have at least 1 y-axis
|
||||
//Addition axes ids will be the MAIN_Y_AXES_ID + x where x is between 1 and MAX_ADDITIONAL_AXES
|
||||
this.additionalYAxes = [];
|
||||
const hasAdditionalAxesConfiguration = Array.isArray(options.model.additionalYAxes);
|
||||
|
||||
for (let yAxisCount = 0; yAxisCount < MAX_ADDITIONAL_AXES; yAxisCount++) {
|
||||
const yAxisId = MAIN_Y_AXES_ID + yAxisCount + 1;
|
||||
const yAxis = hasAdditionalAxesConfiguration && options.model.additionalYAxes.find(additionalYAxis => additionalYAxis?.id === yAxisId);
|
||||
if (yAxis) {
|
||||
this.additionalYAxes.push(new YAxisModel({
|
||||
model: yAxis,
|
||||
plot: this,
|
||||
openmct: options.openmct,
|
||||
id: yAxis.id
|
||||
}));
|
||||
} else {
|
||||
this.additionalYAxes.push(new YAxisModel({
|
||||
plot: this,
|
||||
openmct: options.openmct,
|
||||
id: yAxisId
|
||||
}));
|
||||
}
|
||||
}
|
||||
// end add additional axes
|
||||
|
||||
this.legend = new LegendModel({
|
||||
model: options.model.legend,
|
||||
plot: this,
|
||||
@ -81,6 +111,9 @@ export default class PlotConfigurationModel extends Model {
|
||||
}
|
||||
|
||||
this.yAxis.listenToSeriesCollection(this.series);
|
||||
this.additionalYAxes.forEach(yAxis => {
|
||||
yAxis.listenToSeriesCollection(this.series);
|
||||
});
|
||||
this.legend.listenToSeriesCollection(this.series);
|
||||
|
||||
this.listenTo(this, 'destroy', this.onDestroy, this);
|
||||
@ -145,6 +178,7 @@ export default class PlotConfigurationModel extends Model {
|
||||
domainObject: options.domainObject,
|
||||
xAxis: {},
|
||||
yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}),
|
||||
additionalYAxes: _.cloneDeep(options.domainObject.configuration?.additionalYAxes ?? []),
|
||||
legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {})
|
||||
};
|
||||
}
|
||||
|
@ -118,7 +118,8 @@ export default class PlotSeries extends Model {
|
||||
markerShape: 'point',
|
||||
markerSize: 2.0,
|
||||
alarmMarkers: true,
|
||||
limitLines: false
|
||||
limitLines: false,
|
||||
yAxisId: options.model.yAxisId || 1
|
||||
};
|
||||
}
|
||||
|
||||
@ -378,6 +379,7 @@ export default class PlotSeries extends Model {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a point to the data array while maintaining the sort order of
|
||||
* the array and preventing insertion of points with a duplicate x
|
||||
|
@ -56,6 +56,13 @@ export default class SeriesCollection extends Collection {
|
||||
const series = this.byIdentifier(seriesConfig.identifier);
|
||||
if (series) {
|
||||
series.persistedConfig = seriesConfig;
|
||||
if (!series.persistedConfig.yAxisId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (series.get('yAxisId') !== series.persistedConfig.yAxisId) {
|
||||
series.set('yAxisId', series.persistedConfig.yAxisId);
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
@ -135,18 +135,44 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
}
|
||||
resetStats() {
|
||||
//TODO: do we need the series id here?
|
||||
this.unset('stats');
|
||||
this.seriesCollection.forEach(series => {
|
||||
this.getSeriesForYAxis(this.seriesCollection).forEach(series => {
|
||||
if (series.has('stats')) {
|
||||
this.updateStats(series.get('stats'));
|
||||
}
|
||||
});
|
||||
}
|
||||
getSeriesForYAxis(seriesCollection) {
|
||||
return seriesCollection.filter(series => {
|
||||
const seriesYAxisId = series.get('yAxisId') || 1;
|
||||
|
||||
return seriesYAxisId === this.id;
|
||||
});
|
||||
}
|
||||
|
||||
getYAxisForId(id) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
let yAxis;
|
||||
if (this.id === 1) {
|
||||
yAxis = plotModel.configuration?.yAxis;
|
||||
} else {
|
||||
if (plotModel.configuration?.additionalYAxes) {
|
||||
yAxis = plotModel.configuration.additionalYAxes.find(additionalYAxis => additionalYAxis.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
return yAxis;
|
||||
}
|
||||
/**
|
||||
* @param {import('./PlotSeries').default} series
|
||||
*/
|
||||
trackSeries(series) {
|
||||
this.listenTo(series, 'change:stats', seriesStats => {
|
||||
if (series.get('yAxisId') !== this.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!seriesStats) {
|
||||
this.resetStats();
|
||||
} else {
|
||||
@ -154,8 +180,24 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
});
|
||||
this.listenTo(series, 'change:yKey', () => {
|
||||
if (series.get('yAxisId') !== this.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
});
|
||||
|
||||
this.listenTo(series, 'change:yAxisId', (newYAxisId, oldYAxisId) => {
|
||||
if (oldYAxisId && this.id === oldYAxisId) {
|
||||
this.resetStats();
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
|
||||
if (series.get('yAxisId') === this.id) {
|
||||
this.resetStats();
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
});
|
||||
}
|
||||
untrackSeries(series) {
|
||||
this.stopListening(series);
|
||||
@ -252,14 +294,40 @@ export default class YAxisModel extends Model {
|
||||
// Update the series collection labels and formatting
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given series collection, get the metadata of the current yKey for each series.
|
||||
* Then return first available value of the given property from the metadata.
|
||||
* @param {import('./SeriesCollection').default} series
|
||||
* @param {String} property
|
||||
*/
|
||||
getMetadataValueByProperty(series, property) {
|
||||
return series.map(s => (s.metadata ? s.metadata.value(s.get('yKey'))[property] : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
}
|
||||
/**
|
||||
* Update yAxis format, values, and label from known series.
|
||||
* @param {import('./SeriesCollection').default} seriesCollection
|
||||
*/
|
||||
updateFromSeries(seriesCollection) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
const label = plotModel.configuration?.yAxis?.label;
|
||||
const sampleSeries = seriesCollection.first();
|
||||
const seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection);
|
||||
if (!seriesForThisYAxis.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yAxis = this.getYAxisForId(this.id);
|
||||
const label = yAxis?.label;
|
||||
const sampleSeries = seriesForThisYAxis[0];
|
||||
if (!sampleSeries || !sampleSeries.metadata) {
|
||||
if (!label) {
|
||||
this.unset('label');
|
||||
@ -279,41 +347,17 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
|
||||
this.set('values', yMetadata.values);
|
||||
|
||||
if (!label) {
|
||||
const labelName = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).name : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name');
|
||||
if (labelName) {
|
||||
this.set('label', labelName);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const labelUnits = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).units : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
//if the name is not available, set the units as the label
|
||||
const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units');
|
||||
if (labelUnits) {
|
||||
this.set('label', labelUnits);
|
||||
|
||||
@ -331,7 +375,8 @@ export default class YAxisModel extends Model {
|
||||
frozen: false,
|
||||
autoscale: true,
|
||||
logMode: options.model?.logMode ?? false,
|
||||
autoscalePadding: 0.1
|
||||
autoscalePadding: 0.1,
|
||||
id: options.id
|
||||
|
||||
// 'range' is not specified here, it is undefined at first. When the
|
||||
// user turns off autoscale, the current 'displayRange' is used for
|
||||
|
@ -38,7 +38,7 @@ export default {
|
||||
PlotOptionsBrowse,
|
||||
PlotOptionsEdit
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: this.openmct.editor.isEditing()
|
||||
|
@ -36,20 +36,21 @@
|
||||
/>
|
||||
</ul>
|
||||
<div
|
||||
v-if="plotSeries.length"
|
||||
v-if="plotSeries.length && !isStackedPlotObject"
|
||||
class="grid-properties"
|
||||
>
|
||||
<ul
|
||||
v-if="!isStackedPlotObject"
|
||||
v-for="(yAxis, index) in yAxesWithSeries"
|
||||
:key="`yAxis-${index}`"
|
||||
class="l-inspector-part js-yaxis-properties"
|
||||
>
|
||||
<h2 title="Y axis settings for this object">Y Axis</h2>
|
||||
<h2 title="Y axis settings for this object">Y Axis {{ yAxesWithSeries.length > 1 ? yAxis.id : '' }}</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Manually override how the Y axis is labeled."
|
||||
>Label</div>
|
||||
<div class="grid-cell value">{{ label ? label : "Not defined" }}</div>
|
||||
<div class="grid-cell value">{{ yAxis.label ? yAxis.label : "Not defined" }}</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
@ -57,7 +58,7 @@
|
||||
title="Enable log mode."
|
||||
>Log mode</div>
|
||||
<div class="grid-cell value">
|
||||
{{ logMode ? "Enabled" : "Disabled" }}
|
||||
{{ yAxis.logMode ? "Enabled" : "Disabled" }}
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
@ -66,32 +67,36 @@
|
||||
title="Automatically scale the Y axis to keep all values in view."
|
||||
>Auto scale</div>
|
||||
<div class="grid-cell value">
|
||||
{{ autoscale ? "Enabled: " + autoscalePadding : "Disabled" }}
|
||||
{{ yAxis.autoscale ? "Enabled: " + yAxis.autoscalePadding : "Disabled" }}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="!autoscale && rangeMin"
|
||||
v-if="!yAxis.autoscale && yAxis.rangeMin"
|
||||
class="grid-row"
|
||||
>
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Minimum Y axis value."
|
||||
>Minimum value</div>
|
||||
<div class="grid-cell value">{{ rangeMin }}</div>
|
||||
<div class="grid-cell value">{{ yAxis.rangeMin }}</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="!autoscale && rangeMax"
|
||||
v-if="!yAxis.autoscale && yAxis.rangeMax"
|
||||
class="grid-row"
|
||||
>
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Maximum Y axis value."
|
||||
>Maximum value</div>
|
||||
<div class="grid-cell value">{{ rangeMax }}</div>
|
||||
<div class="grid-cell value">{{ yAxis.rangeMax }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
v-if="plotSeries.length && (isStackedPlotObject || !isNestedWithinAStackedPlot)"
|
||||
class="grid-properties"
|
||||
>
|
||||
<ul
|
||||
v-if="isStackedPlotObject || !isNestedWithinAStackedPlot"
|
||||
class="l-inspector-part js-legend-properties"
|
||||
>
|
||||
<h2 title="Legend settings for this object">Legend</h2>
|
||||
@ -157,12 +162,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
config: {},
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
position: '',
|
||||
hideLegendWhenSmall: '',
|
||||
expandByDefault: '',
|
||||
@ -173,22 +172,28 @@ export default {
|
||||
showMaximumWhenExpanded: '',
|
||||
showUnitsWhenExpanded: '',
|
||||
loaded: false,
|
||||
plotSeries: []
|
||||
plotSeries: [],
|
||||
yAxes: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isNestedWithinAStackedPlot() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
isStackedPlotObject() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
yAxesWithSeries() {
|
||||
return this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.config = this.getConfig();
|
||||
this.initYAxesConfiguration();
|
||||
|
||||
this.registerListeners();
|
||||
this.initConfiguration();
|
||||
this.initLegendConfiguration();
|
||||
this.loaded = true;
|
||||
|
||||
},
|
||||
@ -196,18 +201,38 @@ export default {
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
initConfiguration() {
|
||||
initYAxesConfiguration() {
|
||||
if (this.config) {
|
||||
this.label = this.config.yAxis.get('label');
|
||||
this.autoscale = this.config.yAxis.get('autoscale');
|
||||
this.logMode = this.config.yAxis.get('logMode');
|
||||
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
|
||||
const range = this.config.yAxis.get('range');
|
||||
if (range) {
|
||||
this.rangeMin = range.min;
|
||||
this.rangeMax = range.max;
|
||||
}
|
||||
let range = this.config.yAxis.get('range');
|
||||
|
||||
this.yAxes.push({
|
||||
id: this.config.yAxis.id,
|
||||
seriesCount: 0,
|
||||
label: this.config.yAxis.get('label'),
|
||||
autoscale: this.config.yAxis.get('autoscale'),
|
||||
logMode: this.config.yAxis.get('logMode'),
|
||||
autoscalePadding: this.config.yAxis.get('autoscalePadding'),
|
||||
rangeMin: range ? range.min : '',
|
||||
rangeMax: range ? range.max : ''
|
||||
});
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
range = yAxis.get('range');
|
||||
|
||||
this.yAxes.push({
|
||||
id: yAxis.id,
|
||||
seriesCount: 0,
|
||||
label: yAxis.get('label'),
|
||||
autoscale: yAxis.get('autoscale'),
|
||||
logMode: yAxis.get('logMode'),
|
||||
autoscalePadding: yAxis.get('autoscalePadding'),
|
||||
rangeMin: range ? range.min : '',
|
||||
rangeMax: range ? range.max : ''
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
initLegendConfiguration() {
|
||||
if (this.config) {
|
||||
this.position = this.config.legend.get('position');
|
||||
this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
|
||||
this.expandByDefault = this.config.legend.get('expandByDefault');
|
||||
@ -229,18 +254,44 @@ export default {
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
}
|
||||
},
|
||||
|
||||
setYAxisLabel(yAxisId) {
|
||||
const found = this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
if (found && found.seriesCount > 0) {
|
||||
const mainYAxisId = this.config.yAxis.id;
|
||||
if (mainYAxisId === yAxisId) {
|
||||
found.label = this.config.yAxis.get('label');
|
||||
} else {
|
||||
const additionalYAxis = this.config.additionalYAxes.find(axis => axis.id === yAxisId);
|
||||
if (additionalYAxis) {
|
||||
found.label = additionalYAxis.get('label');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
this.$set(this.plotSeries, index, series);
|
||||
this.initConfiguration();
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
resetAllSeries() {
|
||||
this.plotSeries = [];
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
removeSeries(plotSeries, index) {
|
||||
const yAxisId = plotSeries.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, -1);
|
||||
this.plotSeries.splice(index, 1);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
updateAxisUsageCount(yAxisId, updateCount) {
|
||||
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -40,8 +40,10 @@
|
||||
</li>
|
||||
</ul>
|
||||
<y-axis-form
|
||||
v-if="plotSeries.length && !isStackedPlotObject"
|
||||
class="grid-properties"
|
||||
v-for="(yAxisId, index) in yAxesIds"
|
||||
:id="yAxisId.id"
|
||||
:key="`yAxis-${index}`"
|
||||
class="grid-properties js-yaxis-grid-properties"
|
||||
:y-axis="config.yAxis"
|
||||
@seriesUpdated="updateSeriesConfigForObject"
|
||||
/>
|
||||
@ -76,21 +78,38 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
config: {},
|
||||
yAxes: [],
|
||||
plotSeries: [],
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isStackedPlotNestedObject() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
isStackedPlotObject() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
yAxesIds() {
|
||||
return !this.isStackedPlotObject && this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.config = this.getConfig();
|
||||
this.yAxes = [{
|
||||
id: this.config.yAxis.id,
|
||||
seriesCount: 0
|
||||
}];
|
||||
if (this.config.additionalYAxes) {
|
||||
this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => {
|
||||
return {
|
||||
id: yAxis.id,
|
||||
seriesCount: 0
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
this.registerListeners();
|
||||
this.loaded = true;
|
||||
},
|
||||
@ -107,16 +126,47 @@ export default {
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
},
|
||||
|
||||
findYAxisForId(yAxisId) {
|
||||
return this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
},
|
||||
|
||||
setYAxisLabel(yAxisId) {
|
||||
const found = this.findYAxisForId(yAxisId);
|
||||
if (found && found.seriesCount > 0) {
|
||||
const mainYAxisId = this.config.yAxis.id;
|
||||
if (mainYAxisId === yAxisId) {
|
||||
found.label = this.config.yAxis.get('label');
|
||||
} else {
|
||||
const additionalYAxis = this.config.additionalYAxes.find(axis => axis.id === yAxisId);
|
||||
if (additionalYAxis) {
|
||||
found.label = additionalYAxis.get('label');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
this.$set(this.plotSeries, index, series);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
resetAllSeries() {
|
||||
this.plotSeries = [];
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
removeSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, -1);
|
||||
this.plotSeries.splice(index, 1);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
updateAxisUsageCount(yAxisId, updateCount) {
|
||||
const foundYAxis = this.findYAxisForId(yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;
|
||||
}
|
||||
},
|
||||
|
||||
updateSeriesConfigForObject(config) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="loaded">
|
||||
<ul class="l-inspector-part">
|
||||
<h2>Y Axis</h2>
|
||||
<h2>Y Axis {{ id > 1 ? id : '' }}</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
@ -25,6 +25,7 @@
|
||||
<!-- eslint-disable-next-line vue/html-self-closing -->
|
||||
<input
|
||||
v-model="logMode"
|
||||
class="js-log-mode-input"
|
||||
type="checkbox"
|
||||
@change="updateForm('logMode')"
|
||||
/>
|
||||
@ -103,52 +104,72 @@
|
||||
<script>
|
||||
import { objectPath } from "./formUtil";
|
||||
import _ from "lodash";
|
||||
import eventHelpers from "../../lib/eventHelpers";
|
||||
import configStore from "../../configuration/ConfigStore";
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
yAxis: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
yAxis: null,
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
validationErrors: {}
|
||||
validationErrors: {},
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initialize();
|
||||
eventHelpers.extend(this);
|
||||
this.getConfig();
|
||||
this.loaded = true;
|
||||
this.initFields();
|
||||
this.initFormValues();
|
||||
},
|
||||
methods: {
|
||||
initialize: function () {
|
||||
getConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
const config = configStore.get(configId);
|
||||
if (config) {
|
||||
const mainYAxisId = config.yAxis.id;
|
||||
this.isAdditionalYAxis = this.id !== mainYAxisId;
|
||||
if (this.isAdditionalYAxis) {
|
||||
this.additionalYAxes = config.additionalYAxes;
|
||||
this.yAxis = config.additionalYAxes.find(yAxis => yAxis.id === this.id);
|
||||
} else {
|
||||
this.yAxis = config.yAxis;
|
||||
}
|
||||
}
|
||||
},
|
||||
initFields() {
|
||||
const prefix = `configuration.${this.getPrefix()}`;
|
||||
this.fields = {
|
||||
label: {
|
||||
objectPath: 'configuration.yAxis.label'
|
||||
objectPath: `${prefix}.label`
|
||||
},
|
||||
autoscale: {
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.yAxis.autoscale'
|
||||
objectPath: `${prefix}.autoscale`
|
||||
},
|
||||
autoscalePadding: {
|
||||
coerce: Number,
|
||||
objectPath: 'configuration.yAxis.autoscalePadding'
|
||||
objectPath: `${prefix}.autoscalePadding`
|
||||
},
|
||||
logMode: {
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.yAxis.logMode'
|
||||
objectPath: `${prefix}.logMode`
|
||||
},
|
||||
range: {
|
||||
objectPath: 'configuration.yAxis.range',
|
||||
objectPath: `${prefix}.range'`,
|
||||
coerce: function coerceRange(range) {
|
||||
const newRange = {
|
||||
min: -1,
|
||||
@ -202,6 +223,25 @@ export default {
|
||||
this.rangeMin = range?.min;
|
||||
this.rangeMax = range?.max;
|
||||
},
|
||||
getPrefix() {
|
||||
let prefix = 'yAxis';
|
||||
if (this.isAdditionalYAxis) {
|
||||
let index = -1;
|
||||
if (this.domainObject?.configuration?.additionalYAxes) {
|
||||
index = this.domainObject?.configuration?.additionalYAxes.findIndex((yAxis) => {
|
||||
return yAxis.id === this.id;
|
||||
});
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
prefix = `additionalYAxes[${index}]`;
|
||||
}
|
||||
|
||||
return prefix;
|
||||
},
|
||||
updateForm(formKey) {
|
||||
let newVal;
|
||||
if (formKey === 'range') {
|
||||
@ -231,18 +271,42 @@ export default {
|
||||
this.yAxis.set(formKey, newVal);
|
||||
// Then we mutate the domain object configuration to persist the settings
|
||||
if (path) {
|
||||
if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
|
||||
this.$emit('seriesUpdated', {
|
||||
identifier: this.domainObject.identifier,
|
||||
path: `yAxis.${formKey}`,
|
||||
value: newVal
|
||||
});
|
||||
if (this.isAdditionalYAxis) {
|
||||
if (this.domainObject.configuration && this.domainObject.configuration.series) {
|
||||
//update the id
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
`configuration.${this.getPrefix()}.id`,
|
||||
this.id
|
||||
);
|
||||
//update the yAxes values
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
newVal
|
||||
);
|
||||
} else {
|
||||
this.$emit('seriesUpdated', {
|
||||
identifier: this.domainObject.identifier,
|
||||
path: `${this.getPrefix()}.${formKey}`,
|
||||
id: this.id,
|
||||
value: newVal
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
newVal
|
||||
);
|
||||
if (this.domainObject.configuration && this.domainObject.configuration.series) {
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
newVal
|
||||
);
|
||||
} else {
|
||||
this.$emit('seriesUpdated', {
|
||||
identifier: this.domainObject.identifier,
|
||||
path: `${this.getPrefix()}.${formKey}`,
|
||||
value: newVal
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|